@pie-players/pie-assessment-toolkit 0.3.49 → 0.3.51

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 CHANGED
@@ -616,12 +616,37 @@ tools: {
616
616
  }
617
617
  ```
618
618
 
619
+ Hosts that need semantic UI copy can use object-form options. The `rate` still
620
+ drives playback and provider mapping; `label` / `ariaLabel` only change visible
621
+ and accessible button text.
622
+
623
+ ```typescript
624
+ tools: {
625
+ providers: {
626
+ textToSpeech: {
627
+ enabled: true,
628
+ backend: "server",
629
+ serverProvider: "custom",
630
+ settings: {
631
+ speedOptions: [
632
+ { rate: 0.8, label: "Slow", ariaLabel: "Slow speed" },
633
+ { rate: 1.5, label: "Fast", ariaLabel: "Fast speed" }
634
+ ]
635
+ }
636
+ }
637
+ }
638
+ }
639
+ ```
640
+
619
641
  `speedOptions` semantics:
620
642
 
621
643
  - Omitted or non-array: default speed buttons are shown (`0.8x`, `1.25x`).
622
644
  - Explicit empty array (`[]`): hide all speed buttons.
623
645
  - Invalid-only arrays (for example `["fast", -1, 1]`): fall back to defaults.
624
646
  - Valid numeric values are deduplicated and keep first-seen order.
647
+ - Object-form entries use the same numeric validation/deduping by `rate`.
648
+ - Missing labels fall back to numeric text like `1.5x`; missing `ariaLabel`
649
+ falls back to a matching accessible name like `Fast speed`.
625
650
  - `1` is excluded (normal speed is already available by toggling active speed off).
626
651
 
627
652
  ### Runtime Fallback: Server TTS -> Browser TTS
@@ -6871,22 +6871,81 @@ var isServerBackend = (backend) => backend === "polly" || backend === "google" |
6871
6871
  var withDefault = (value, fallback2) => value === undefined ? fallback2 : value;
6872
6872
  var normalizeTTSLayoutMode = (value, fallback2 = "left-aligned") => typeof value === "string" && VALID_TTS_LAYOUT_MODES.has(value) ? value : fallback2;
6873
6873
  var DEFAULT_TTS_SPEED_OPTIONS = Object.freeze([0.8, 1.25]);
6874
- var normalizeTTSSpeedOptions = (value) => {
6874
+ var normalizeSpeedRate = (entry) => {
6875
+ if (typeof entry !== "number" || !Number.isFinite(entry) || entry <= 0) {
6876
+ return;
6877
+ }
6878
+ const rounded = Math.round(entry * 100) / 100;
6879
+ return rounded === 1 ? undefined : rounded;
6880
+ };
6881
+ var trimOptionalText = (value) => {
6882
+ if (typeof value !== "string")
6883
+ return;
6884
+ const trimmed = value.trim();
6885
+ return trimmed.length ? trimmed : undefined;
6886
+ };
6887
+ var formatSpeedLabel = (rate) => `${rate}x`;
6888
+ var formatSpeedAriaLabel = (label, usedDefaultLabel) => usedDefaultLabel ? `Speed ${label}` : `${label} speed`;
6889
+ var normalizeSpeedAriaLabel = (label, ariaLabel, usedDefaultLabel) => {
6890
+ if (!ariaLabel)
6891
+ return formatSpeedAriaLabel(label, usedDefaultLabel);
6892
+ if (ariaLabel.toLowerCase().includes(label.toLowerCase()))
6893
+ return ariaLabel;
6894
+ return `${label} ${ariaLabel}`;
6895
+ };
6896
+ var normalizeTTSSpeedOptionConfig = (entry) => {
6897
+ if (typeof entry === "number")
6898
+ return normalizeSpeedRate(entry);
6899
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
6900
+ return;
6901
+ const record = entry;
6902
+ const rate = normalizeSpeedRate(record.rate);
6903
+ if (rate === undefined)
6904
+ return;
6905
+ const label = trimOptionalText(record.label);
6906
+ const ariaLabel = trimOptionalText(record.ariaLabel);
6907
+ return {
6908
+ rate,
6909
+ ...label ? { label } : {},
6910
+ ...ariaLabel ? { ariaLabel } : {}
6911
+ };
6912
+ };
6913
+ var normalizeTTSSpeedOptionConfigs = (value) => {
6875
6914
  if (!Array.isArray(value))
6876
6915
  return [...DEFAULT_TTS_SPEED_OPTIONS];
6877
6916
  if (value.length === 0)
6878
6917
  return [];
6879
- const deduped = new Set;
6918
+ const dedupedRates = new Set;
6919
+ const normalized = [];
6880
6920
  for (const entry of value) {
6881
- if (typeof entry !== "number" || !Number.isFinite(entry) || entry <= 0)
6921
+ const option = normalizeTTSSpeedOptionConfig(entry);
6922
+ if (option === undefined)
6882
6923
  continue;
6883
- const rounded = Math.round(entry * 100) / 100;
6884
- if (rounded === 1)
6924
+ const rate = typeof option === "number" ? option : option.rate;
6925
+ if (dedupedRates.has(rate))
6885
6926
  continue;
6886
- deduped.add(rounded);
6927
+ dedupedRates.add(rate);
6928
+ normalized.push(option);
6887
6929
  }
6888
- return deduped.size ? Array.from(deduped) : [...DEFAULT_TTS_SPEED_OPTIONS];
6930
+ return normalized.length ? normalized : [...DEFAULT_TTS_SPEED_OPTIONS];
6889
6931
  };
6932
+ var normalizeTTSSpeedControlOptions = (value) => normalizeTTSSpeedOptionConfigs(value).map((option) => {
6933
+ if (typeof option === "number") {
6934
+ const label2 = formatSpeedLabel(option);
6935
+ return {
6936
+ rate: option,
6937
+ label: label2,
6938
+ ariaLabel: formatSpeedAriaLabel(label2, true)
6939
+ };
6940
+ }
6941
+ const defaultLabel = formatSpeedLabel(option.rate);
6942
+ const label = option.label || defaultLabel;
6943
+ return {
6944
+ rate: option.rate,
6945
+ label,
6946
+ ariaLabel: normalizeSpeedAriaLabel(label, option.ariaLabel, label === defaultLabel)
6947
+ };
6948
+ });
6890
6949
  var applyRuntimeDefaults = (config) => {
6891
6950
  const withLayoutDefaults = {
6892
6951
  ...config,
@@ -7043,7 +7102,7 @@ var ttsToolRegistration = {
7043
7102
  settings.layoutMode = normalizeTTSLayoutMode(settings.layoutMode);
7044
7103
  }
7045
7104
  if (settings && "speedOptions" in settings) {
7046
- settings.speedOptions = normalizeTTSSpeedOptions(settings.speedOptions);
7105
+ settings.speedOptions = normalizeTTSSpeedOptionConfigs(settings.speedOptions);
7047
7106
  }
7048
7107
  const normalizedConfig = {
7049
7108
  ...config
@@ -7052,7 +7111,7 @@ var ttsToolRegistration = {
7052
7111
  normalizedConfig.layoutMode = normalizeTTSLayoutMode(normalizedConfig.layoutMode);
7053
7112
  }
7054
7113
  if ("speedOptions" in normalizedConfig) {
7055
- normalizedConfig.speedOptions = normalizeTTSSpeedOptions(normalizedConfig.speedOptions);
7114
+ normalizedConfig.speedOptions = normalizeTTSSpeedOptionConfigs(normalizedConfig.speedOptions);
7056
7115
  }
7057
7116
  if (settings) {
7058
7117
  normalizedConfig.settings = settings;
@@ -7075,7 +7134,7 @@ var ttsToolRegistration = {
7075
7134
  const resolveRuntimeSettings = () => resolveTTSRuntimeSettings(toolbarContext.toolkitCoordinator?.getToolConfig(this.toolId) || undefined);
7076
7135
  const resolveElementSpeedOptions = () => {
7077
7136
  const runtimeSettings = resolveRuntimeSettings();
7078
- return normalizeTTSSpeedOptions(runtimeSettings.speedOptions);
7137
+ return normalizeTTSSpeedControlOptions(runtimeSettings.speedOptions);
7079
7138
  };
7080
7139
  const resolveLayoutMode = () => resolveTTSLayoutMode(resolveRuntimeSettings());
7081
7140
  const resolveHostLayout = () => resolveTTSHostToolbarLayout(resolveRuntimeSettings());
@@ -7718,21 +7777,6 @@ function createPackagedToolRegistry(options = {}) {
7718
7777
  });
7719
7778
  }
7720
7779
 
7721
- // dist/tools/default-tool-module-loaders.js
7722
- var loadSideEffectModule = (load) => load().then(() => {
7723
- return;
7724
- });
7725
- function loadCalculatorModule() {
7726
- if (typeof globalThis !== "undefined" && "customElements" in globalThis && globalThis.customElements?.get("pie-tool-calculator")) {
7727
- return Promise.resolve();
7728
- }
7729
- return loadSideEffectModule(() => import("@pie-players/pie-tool-calculator-desmos"));
7730
- }
7731
- var DEFAULT_TOOL_MODULE_LOADERS = {
7732
- calculator: loadCalculatorModule,
7733
- textToSpeech: () => loadSideEffectModule(() => import("@pie-players/pie-tool-tts-inline"))
7734
- };
7735
-
7736
7780
  // dist/components/vendor/nds/nds-icon-button.js
7737
7781
  var H = globalThis;
7738
7782
  var D = H.ShadowRoot && (H.ShadyCSS === undefined || H.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype;
@@ -8707,7 +8751,7 @@ function ItemToolBar($$anchor, $$props) {
8707
8751
  installFaInToolbarShadow(node);
8708
8752
  return {};
8709
8753
  };
8710
- const fallbackToolRegistry = createPackagedToolRegistry({ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS });
8754
+ const fallbackToolRegistry = createPackagedToolRegistry();
8711
8755
  let activeToolState = state(proxy({}));
8712
8756
  let level = prop($$props, "level", 7, "item"), scopeId = prop($$props, "scopeId", 7, ""), itemId = prop($$props, "itemId", 7, ""), sectionId = prop($$props, "sectionId", 7, ""), catalogId = prop($$props, "catalogId", 7, ""), tools = prop($$props, "tools", 7, "calculator,textToSpeech,answerEliminator"), contentKind = prop($$props, "contentKind", 7, "assessment-item"), position = prop($$props, "position", 7, "bottom"), scopeElement = prop($$props, "scopeElement", 7, null), toolRegistry = prop($$props, "toolRegistry", 7, null), item = prop($$props, "item", 7, null), hostButtons = prop($$props, "hostButtons", 23, () => []), className = prop($$props, "class", 7, ""), size = prop($$props, "size", 7, "md"), language = prop($$props, "language", 7, "en-US");
8713
8757
  let toolbarRootElement = state(null);
@@ -9349,13 +9393,34 @@ function ItemToolBar($$anchor, $$props) {
9349
9393
  const maxHeight = shellConfig?.maxHeight ?? window.innerHeight;
9350
9394
  return { minWidth, minHeight, maxWidth, maxHeight };
9351
9395
  };
9396
+ const applyContentMinWidth = () => {
9397
+ const configuredMinWidth = currentArgs.mounted.entry.shell?.minWidth;
9398
+ const shouldScroll = typeof configuredMinWidth === "number" && configuredMinWidth > 0 && width < configuredMinWidth;
9399
+ const minWidthValue = shouldScroll ? `${configuredMinWidth}px` : "";
9400
+ if (mountedContentElement) {
9401
+ mountedContentElement.style.minWidth = minWidthValue;
9402
+ }
9403
+ if (headerEl) {
9404
+ headerEl.style.minWidth = minWidthValue;
9405
+ }
9406
+ if (contentEl) {
9407
+ contentEl.style.minWidth = minWidthValue;
9408
+ }
9409
+ if (shellEl) {
9410
+ shellEl.style.overflowX = shouldScroll ? "auto" : "visible";
9411
+ shellEl.style.overflowY = "visible";
9412
+ }
9413
+ };
9352
9414
  const applyPositionAndSize = () => {
9353
9415
  const { minWidth, minHeight, maxWidth, maxHeight } = getShellBounds();
9354
- width = clamp(width, minWidth, Math.max(minWidth, Math.min(maxWidth, window.innerWidth)));
9355
- height = clamp(height, minHeight, Math.max(minHeight, Math.min(maxHeight, window.innerHeight)));
9416
+ const effectiveMinWidth = Math.min(minWidth, window.innerWidth);
9417
+ const effectiveMinHeight = Math.min(minHeight, window.innerHeight);
9418
+ width = clamp(width, effectiveMinWidth, Math.max(effectiveMinWidth, Math.min(maxWidth, window.innerWidth)));
9419
+ height = clamp(height, effectiveMinHeight, Math.max(effectiveMinHeight, Math.min(maxHeight, window.innerHeight)));
9356
9420
  x2 = clamp(x2, 0, Math.max(0, window.innerWidth - width));
9357
9421
  y2 = clamp(y2, 0, Math.max(0, window.innerHeight - height));
9358
9422
  applyShellStyle();
9423
+ applyContentMinWidth();
9359
9424
  notifyHostedResize();
9360
9425
  };
9361
9426
  const moveBy = (dx, dy) => {
@@ -9456,6 +9521,7 @@ function ItemToolBar($$anchor, $$props) {
9456
9521
  element2.style.height = "100%";
9457
9522
  element2.style.flex = "1 1 auto";
9458
9523
  element2.style.minHeight = "0";
9524
+ applyContentMinWidth();
9459
9525
  if (mountedContentElement && mountedContentElement !== element2) {
9460
9526
  if (mountedContentElement.parentNode === contentEl) {
9461
9527
  invokeElementUnmount(mountedContentElement);
@@ -9704,8 +9770,10 @@ function ItemToolBar($$anchor, $$props) {
9704
9770
  if (resizePointerId === event2.pointerId) {
9705
9771
  event2.preventDefault();
9706
9772
  const shellConfig = currentArgs.mounted.entry.shell;
9707
- const minWidth = shellConfig?.minWidth ?? 320;
9708
- const minHeight = shellConfig?.minHeight ?? 240;
9773
+ const configuredMinWidth = shellConfig?.minWidth ?? 320;
9774
+ const configuredMinHeight = shellConfig?.minHeight ?? 240;
9775
+ const minWidth = Math.min(configuredMinWidth, window.innerWidth);
9776
+ const minHeight = Math.min(configuredMinHeight, window.innerHeight);
9709
9777
  const maxWidth = shellConfig?.maxWidth ?? window.innerWidth;
9710
9778
  const maxHeight = shellConfig?.maxHeight ?? window.innerHeight;
9711
9779
  const dx = event2.clientX - resizeStartX;
@@ -9736,6 +9804,7 @@ function ItemToolBar($$anchor, $$props) {
9736
9804
  height = newHeight;
9737
9805
  }
9738
9806
  applyShellStyle();
9807
+ applyContentMinWidth();
9739
9808
  notifyHostedResize();
9740
9809
  }
9741
9810
  };
@@ -39,7 +39,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
39
39
 
40
40
  // ../../node_modules/.bun/speech-rule-engine@5.0.0-rc.1/node_modules/speech-rule-engine/lib/sre.js
41
41
  var require_sre = __commonJS((exports, module) => {
42
- var __dirname = "/Users/eelco.hillenius/dev/prj/pie/pie-players/node_modules/.bun/speech-rule-engine@5.0.0-rc.1/node_modules/speech-rule-engine/lib";
42
+ var __dirname = "/home/runner/work/pie-players/pie-players/node_modules/.bun/speech-rule-engine@5.0.0-rc.1/node_modules/speech-rule-engine/lib";
43
43
  (function(t, e) {
44
44
  typeof exports == "object" && typeof module == "object" ? module.exports = e() : typeof define == "function" && define.amd ? define([], e) : typeof exports == "object" ? exports.SRE = e() : t.SRE = e();
45
45
  })(exports, () => (() => {
@@ -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());
@@ -23839,6 +23898,7 @@ class TTSService {
23839
23898
  lastError = null;
23840
23899
  speakRunId = 0;
23841
23900
  currentBoundaryOffset = 0;
23901
+ activeWordBoundaryOffset = 0;
23842
23902
  seekSegments = [];
23843
23903
  sentenceHighlightSegments = [];
23844
23904
  currentSeekSegmentIndex = 0;
@@ -24232,11 +24292,11 @@ class TTSService {
24232
24292
  this.currentBoundaryOffset = 0;
24233
24293
  const originalOnWordBoundary = this.provider.onWordBoundary;
24234
24294
  this.provider.onWordBoundary = (word, position, length) => {
24295
+ originalOnWordBoundary?.(word, position, length);
24235
24296
  if (Number.isFinite(position)) {
24236
24297
  this.currentBoundaryOffset = position;
24237
24298
  this.currentSeekSegmentIndex = this.getCurrentSeekSegmentIndex();
24238
24299
  }
24239
- originalOnWordBoundary?.(word, position, length);
24240
24300
  };
24241
24301
  try {
24242
24302
  await providerWithPlan.speakSegments(segments);
@@ -24247,6 +24307,10 @@ class TTSService {
24247
24307
  }
24248
24308
  return;
24249
24309
  }
24310
+ this.configureWordBoundaryHighlighting({
24311
+ highlightMode: options?.highlightMode || "word",
24312
+ wordBoundaryOffset: this.activeWordBoundaryOffset
24313
+ });
24250
24314
  for (const segment of segments) {
24251
24315
  if (runId !== this.speakRunId)
24252
24316
  return;
@@ -24366,6 +24430,7 @@ class TTSService {
24366
24430
  this.initializeSpeakTracking(highlightText, options);
24367
24431
  const highlightMode = options?.highlightModeOverride || (speechMatchesVisibleText ? this.resolveHighlightMode() : "sentence");
24368
24432
  this.activeHighlightMode = highlightMode;
24433
+ this.activeWordBoundaryOffset = options?.wordBoundaryOffset || 0;
24369
24434
  const hasExplicitBreaks = this.hasExplicitBreakSemantics(contentToSpeak);
24370
24435
  const shouldUsePlan = !!this.currentContentElement && !usedCatalogSpoken && !hasExplicitBreaks && speechMatchesVisibleText;
24371
24436
  this.seekSegments = hasExplicitBreaks || !speechMatchesVisibleText ? [] : shouldUsePlan && this.currentContentElement ? this.createSpeechPlan(this.currentContentElement, normalizedText) : this.createSeekSegmentsFromText(highlightText);
@@ -24951,6 +25016,7 @@ class TTSService {
24951
25016
  this.currentContentElement = null;
24952
25017
  this.normalizedToDOM.clear();
24953
25018
  this.currentBoundaryOffset = 0;
25019
+ this.activeWordBoundaryOffset = 0;
24954
25020
  this.seekSegments = [];
24955
25021
  this.sentenceHighlightSegments = [];
24956
25022
  this.currentSeekSegmentIndex = 0;
@@ -25005,31 +25071,27 @@ class TTSService {
25005
25071
  this.setState(PlaybackState.PLAYING);
25006
25072
  }
25007
25073
  }
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)
25074
+ normalizePlaybackRate(rate) {
25075
+ if (!Number.isFinite(rate))
25076
+ return 1;
25077
+ return Math.max(0.25, Math.min(4, rate));
25078
+ }
25079
+ async restartFromSeekIndex(targetIndex) {
25080
+ if (!this.provider || this.seekSegments.length === 0)
25024
25081
  return;
25082
+ const safeTargetIndex = Math.max(0, Math.min(this.seekSegments.length - 1, targetIndex));
25025
25083
  this.speakRunId += 1;
25026
25084
  this.provider.onWordBoundary = undefined;
25027
25085
  this.provider.stop();
25028
- this.currentSeekSegmentIndex = targetIndex;
25086
+ this.currentSeekSegmentIndex = safeTargetIndex;
25029
25087
  const runId = ++this.speakRunId;
25030
- const restartSegments = this.seekSegments.slice(targetIndex);
25088
+ const restartSegments = this.seekSegments.slice(safeTargetIndex);
25031
25089
  this.setState(PlaybackState.PLAYING);
25032
25090
  try {
25091
+ this.configureWordBoundaryHighlighting({
25092
+ highlightMode: this.activeHighlightMode,
25093
+ wordBoundaryOffset: this.activeWordBoundaryOffset
25094
+ });
25033
25095
  await this.speakWithPlan(restartSegments, runId, {
25034
25096
  highlightMode: this.activeHighlightMode
25035
25097
  });
@@ -25046,6 +25108,33 @@ class TTSService {
25046
25108
  throw error;
25047
25109
  }
25048
25110
  }
25111
+ async setPlaybackRate(rate) {
25112
+ const nextRate = this.normalizePlaybackRate(rate);
25113
+ await this.updateSettings({ rate: nextRate });
25114
+ if (this.state !== PlaybackState.PLAYING || !this.currentText || this.seekSegments.length === 0 || this.hasExplicitBreakSemantics(this.currentText)) {
25115
+ return;
25116
+ }
25117
+ await this.restartFromSeekIndex(this.getCurrentSeekSegmentIndex());
25118
+ }
25119
+ async seekBy(units) {
25120
+ if (!this.provider || !this.currentText)
25121
+ return;
25122
+ if (this.state !== PlaybackState.PLAYING && this.state !== PlaybackState.PAUSED) {
25123
+ return;
25124
+ }
25125
+ if (this.hasExplicitBreakSemantics(this.currentText))
25126
+ return;
25127
+ if (this.seekSegments.length === 0)
25128
+ return;
25129
+ const delta = Number.isFinite(units) ? Math.trunc(units) : 0;
25130
+ if (delta === 0)
25131
+ return;
25132
+ const currentIndex = this.getCurrentSeekSegmentIndex();
25133
+ const targetIndex = Math.max(0, Math.min(this.seekSegments.length - 1, currentIndex + delta));
25134
+ if (targetIndex === currentIndex)
25135
+ return;
25136
+ await this.restartFromSeekIndex(targetIndex);
25137
+ }
25049
25138
  async seekForward(units = 1) {
25050
25139
  const step = Number.isFinite(units) ? Math.max(1, Math.trunc(units)) : 1;
25051
25140
  await this.seekBy(step);
@@ -25069,6 +25158,7 @@ class TTSService {
25069
25158
  this.currentContentElement = null;
25070
25159
  this.normalizedToDOM.clear();
25071
25160
  this.currentBoundaryOffset = 0;
25161
+ this.activeWordBoundaryOffset = 0;
25072
25162
  this.seekSegments = [];
25073
25163
  this.sentenceHighlightSegments = [];
25074
25164
  this.currentSeekSegmentIndex = 0;