@pie-players/pie-assessment-toolkit 0.3.52 → 0.3.54

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
@@ -609,7 +609,7 @@ tools: {
609
609
  enabled: true,
610
610
  backend: "browser",
611
611
  settings: {
612
- speedOptions: [2, 1.25, 1.5] // rendered in this order
612
+ speedOptions: [2, 1.25, 1.5] // host options keep this order; Normal is added if omitted
613
613
  }
614
614
  }
615
615
  }
@@ -630,6 +630,7 @@ tools: {
630
630
  settings: {
631
631
  speedOptions: [
632
632
  { rate: 0.8, label: "Slow", ariaLabel: "Slow speed" },
633
+ { rate: 1, label: "Normal", ariaLabel: "Normal speed", default: true },
633
634
  { rate: 1.5, label: "Fast", ariaLabel: "Fast speed" }
634
635
  ]
635
636
  }
@@ -640,14 +641,20 @@ tools: {
640
641
 
641
642
  `speedOptions` semantics:
642
643
 
643
- - Omitted or non-array: default speed buttons are shown (`0.8x`, `1.25x`).
644
- - Explicit empty array (`[]`): hide all speed buttons.
645
- - Invalid-only arrays (for example `["fast", -1, 1]`): fall back to defaults.
644
+ - Omitted or non-array: default speed choices are shown (`Slow`, `Normal`, `Fast`), with `Normal` selected.
645
+ - Explicit empty array (`[]`): hide all speed choices and reset playback speed to `1.0`.
646
+ - Invalid-only arrays (for example `["fast", -1]`): fall back to defaults.
646
647
  - Valid numeric values are deduplicated and keep first-seen order.
647
648
  - Object-form entries use the same numeric validation/deduping by `rate`.
648
649
  - Missing labels fall back to numeric text like `1.5x`; missing `ariaLabel`
649
650
  falls back to a matching accessible name like `Fast speed`.
650
- - `1` is excluded (normal speed is already available by toggling active speed off).
651
+ - `1` is a visible `Normal` choice. If a non-empty config omits `1`, PIE adds
652
+ `Normal` at the natural point in the speed scale while preserving the
653
+ relative order of host-provided options.
654
+ - Speed choices are exposed as a `Playback speed` radio group: one option is
655
+ always selected, and choosing the already-selected speed is a no-op.
656
+ - If only one speed option remains, the speed group is hidden by default. Set
657
+ `showSingleSpeedOption: true` in TTS settings to surface that one-option state.
651
658
 
652
659
  ### Runtime Fallback: Server TTS -> Browser TTS
653
660
 
@@ -6809,7 +6809,11 @@ var calculatorToolRegistration = {
6809
6809
  minWidth: 380,
6810
6810
  minHeight: 420,
6811
6811
  initialAlign: "bottom-right",
6812
- initialMargin: 16
6812
+ initialMargin: 16,
6813
+ content: {
6814
+ overflowY: "auto",
6815
+ preserveMinHeight: true
6816
+ }
6813
6817
  }
6814
6818
  }
6815
6819
  ],
@@ -6871,12 +6875,16 @@ var isServerBackend = (backend) => backend === "polly" || backend === "google" |
6871
6875
  var withDefault = (value, fallback2) => value === undefined ? fallback2 : value;
6872
6876
  var normalizeTTSLayoutMode = (value, fallback2 = "left-aligned") => typeof value === "string" && VALID_TTS_LAYOUT_MODES.has(value) ? value : fallback2;
6873
6877
  var DEFAULT_TTS_SPEED_OPTIONS = Object.freeze([0.8, 1.25]);
6874
- var normalizeSpeedRate = (entry) => {
6878
+ var DEFAULT_TTS_SPEED_CONTROL_OPTIONS = Object.freeze([
6879
+ { rate: 0.8, label: "Slow", ariaLabel: "Slow speed" },
6880
+ { rate: 1, label: "Normal", ariaLabel: "Normal speed", default: true },
6881
+ { rate: 1.25, label: "Fast", ariaLabel: "Fast speed" }
6882
+ ]);
6883
+ var normalizeControlSpeedRate = (entry) => {
6875
6884
  if (typeof entry !== "number" || !Number.isFinite(entry) || entry <= 0) {
6876
6885
  return;
6877
6886
  }
6878
- const rounded = Math.round(entry * 100) / 100;
6879
- return rounded === 1 ? undefined : rounded;
6887
+ return Math.round(entry * 100) / 100;
6880
6888
  };
6881
6889
  var trimOptionalText = (value) => {
6882
6890
  if (typeof value !== "string")
@@ -6885,7 +6893,7 @@ var trimOptionalText = (value) => {
6885
6893
  return trimmed.length ? trimmed : undefined;
6886
6894
  };
6887
6895
  var formatSpeedLabel = (rate) => `${rate}x`;
6888
- var formatSpeedAriaLabel = (label, usedDefaultLabel) => usedDefaultLabel ? `Speed ${label}` : `${label} speed`;
6896
+ var formatSpeedAriaLabel = (label, usedDefaultLabel) => label.toLowerCase() === "normal" ? "Normal speed" : usedDefaultLabel ? `Speed ${label}` : `${label} speed`;
6889
6897
  var normalizeSpeedAriaLabel = (label, ariaLabel, usedDefaultLabel) => {
6890
6898
  if (!ariaLabel)
6891
6899
  return formatSpeedAriaLabel(label, usedDefaultLabel);
@@ -6893,59 +6901,55 @@ var normalizeSpeedAriaLabel = (label, ariaLabel, usedDefaultLabel) => {
6893
6901
  return ariaLabel;
6894
6902
  return `${label} ${ariaLabel}`;
6895
6903
  };
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) => {
6914
- if (!Array.isArray(value))
6915
- return [...DEFAULT_TTS_SPEED_OPTIONS];
6916
- if (value.length === 0)
6904
+ var normalizeTTSSpeedControlOptions = (value) => {
6905
+ const input = Array.isArray(value) ? value : [...DEFAULT_TTS_SPEED_CONTROL_OPTIONS];
6906
+ if (input.length === 0)
6917
6907
  return [];
6918
6908
  const dedupedRates = new Set;
6919
6909
  const normalized = [];
6920
- for (const entry of value) {
6921
- const option = normalizeTTSSpeedOptionConfig(entry);
6922
- if (option === undefined)
6923
- continue;
6924
- const rate = typeof option === "number" ? option : option.rate;
6925
- if (dedupedRates.has(rate))
6910
+ for (const entry of input) {
6911
+ const record = toRecord(entry);
6912
+ const rate = typeof entry === "number" ? normalizeControlSpeedRate(entry) : normalizeControlSpeedRate(record.rate);
6913
+ if (rate === undefined || dedupedRates.has(rate))
6926
6914
  continue;
6927
6915
  dedupedRates.add(rate);
6928
- normalized.push(option);
6916
+ const defaultLabel = rate === 1 ? "Normal" : typeof entry === "number" ? formatSpeedLabel(rate) : formatSpeedLabel(rate);
6917
+ const label = typeof entry === "number" ? defaultLabel : trimOptionalText(record.label) || defaultLabel;
6918
+ const ariaLabel = typeof entry === "number" ? rate === 1 ? "Normal speed" : formatSpeedAriaLabel(label, true) : normalizeSpeedAriaLabel(label, trimOptionalText(record.ariaLabel), label === defaultLabel);
6919
+ normalized.push({
6920
+ rate,
6921
+ label,
6922
+ ariaLabel,
6923
+ isDefault: false,
6924
+ requestedDefault: record.default === true || record.isDefault === true
6925
+ });
6929
6926
  }
6930
- return normalized.length ? normalized : [...DEFAULT_TTS_SPEED_OPTIONS];
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)
6927
+ if (!normalized.length) {
6928
+ return normalizeTTSSpeedControlOptions(DEFAULT_TTS_SPEED_CONTROL_OPTIONS);
6929
+ }
6930
+ if (!dedupedRates.has(1)) {
6931
+ const normalOption = {
6932
+ rate: 1,
6933
+ label: "Normal",
6934
+ ariaLabel: "Normal speed",
6935
+ isDefault: false,
6936
+ requestedDefault: false
6939
6937
  };
6938
+ const firstFasterIndex = normalized.findIndex((option) => option.rate > 1);
6939
+ if (firstFasterIndex >= 0) {
6940
+ normalized.splice(firstFasterIndex, 0, normalOption);
6941
+ } else {
6942
+ normalized.push(normalOption);
6943
+ }
6940
6944
  }
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
- });
6945
+ const requestedDefaultIndex = normalized.findIndex((option) => option.requestedDefault);
6946
+ const defaultIndex = requestedDefaultIndex >= 0 ? requestedDefaultIndex : normalized.findIndex((option) => option.rate === 1);
6947
+ return normalized.map(({ requestedDefault: _requestedDefault, ...option }, index2) => ({
6948
+ ...option,
6949
+ isDefault: index2 === defaultIndex
6950
+ }));
6951
+ };
6952
+ var DEFAULT_TTS_SPEED_CONTROL_RATES = Object.freeze([0.8, 1, 1.25]);
6949
6953
  var applyRuntimeDefaults = (config) => {
6950
6954
  const withLayoutDefaults = {
6951
6955
  ...config,
@@ -7102,7 +7106,7 @@ var ttsToolRegistration = {
7102
7106
  settings.layoutMode = normalizeTTSLayoutMode(settings.layoutMode);
7103
7107
  }
7104
7108
  if (settings && "speedOptions" in settings) {
7105
- settings.speedOptions = normalizeTTSSpeedOptionConfigs(settings.speedOptions);
7109
+ settings.speedOptions = normalizeTTSSpeedControlOptions(settings.speedOptions);
7106
7110
  }
7107
7111
  const normalizedConfig = {
7108
7112
  ...config
@@ -7111,7 +7115,7 @@ var ttsToolRegistration = {
7111
7115
  normalizedConfig.layoutMode = normalizeTTSLayoutMode(normalizedConfig.layoutMode);
7112
7116
  }
7113
7117
  if ("speedOptions" in normalizedConfig) {
7114
- normalizedConfig.speedOptions = normalizeTTSSpeedOptionConfigs(normalizedConfig.speedOptions);
7118
+ normalizedConfig.speedOptions = normalizeTTSSpeedControlOptions(normalizedConfig.speedOptions);
7115
7119
  }
7116
7120
  if (settings) {
7117
7121
  normalizedConfig.settings = settings;
@@ -7164,6 +7168,7 @@ var ttsToolRegistration = {
7164
7168
  element2.setAttribute("size", resolveControlSize());
7165
7169
  element2.setAttribute("layout-mode", resolveLayoutMode());
7166
7170
  element2.speedOptions = resolveElementSpeedOptions();
7171
+ element2.showSingleSpeedOption = resolveRuntimeSettings().showSingleSpeedOption === true;
7167
7172
  return element2;
7168
7173
  };
7169
7174
  const hostLayout = resolveHostLayout();
@@ -7201,6 +7206,7 @@ var ttsToolRegistration = {
7201
7206
  element2.setAttribute("size", resolveControlSize());
7202
7207
  element2.setAttribute("layout-mode", resolveLayoutMode());
7203
7208
  element2.speedOptions = resolveElementSpeedOptions();
7209
+ element2.showSingleSpeedOption = resolveRuntimeSettings().showSingleSpeedOption === true;
7204
7210
  }
7205
7211
  };
7206
7212
  }
@@ -9411,6 +9417,38 @@ function ItemToolBar($$anchor, $$props) {
9411
9417
  shellEl.style.overflowY = "visible";
9412
9418
  }
9413
9419
  };
9420
+ const getContentOverflowY = () => currentArgs.mounted.entry.shell?.content?.overflowY === "auto" ? "auto" : "hidden";
9421
+ const getHeaderHeight = () => {
9422
+ if (!headerEl)
9423
+ return 0;
9424
+ const rectHeight = headerEl.getBoundingClientRect().height;
9425
+ if (rectHeight > 0)
9426
+ return rectHeight;
9427
+ return headerEl.offsetHeight || 0;
9428
+ };
9429
+ const applyContentLayout = () => {
9430
+ if (!contentEl)
9431
+ return;
9432
+ const shellConfig = currentArgs.mounted.entry.shell;
9433
+ contentEl.style.overflowX = "hidden";
9434
+ contentEl.style.overflowY = getContentOverflowY();
9435
+ if (!mountedContentElement)
9436
+ return;
9437
+ if (shellConfig?.content?.preserveMinHeight === true) {
9438
+ const headerHeight = getHeaderHeight();
9439
+ const shellMinHeight = shellConfig.minHeight ?? 240;
9440
+ const contentMinHeight = Math.max(0, shellMinHeight - headerHeight);
9441
+ const availableHeight = Math.max(0, height - headerHeight);
9442
+ const elementHeight = Math.max(availableHeight, contentMinHeight);
9443
+ mountedContentElement.style.height = `${elementHeight}px`;
9444
+ mountedContentElement.style.minHeight = "0";
9445
+ mountedContentElement.style.flex = `0 0 ${elementHeight}px`;
9446
+ return;
9447
+ }
9448
+ mountedContentElement.style.height = "100%";
9449
+ mountedContentElement.style.minHeight = "0";
9450
+ mountedContentElement.style.flex = "1 1 auto";
9451
+ };
9414
9452
  const applyPositionAndSize = () => {
9415
9453
  const { minWidth, minHeight, maxWidth, maxHeight } = getShellBounds();
9416
9454
  const effectiveMinWidth = Math.min(minWidth, window.innerWidth);
@@ -9421,6 +9459,7 @@ function ItemToolBar($$anchor, $$props) {
9421
9459
  y2 = clamp(y2, 0, Math.max(0, window.innerHeight - height));
9422
9460
  applyShellStyle();
9423
9461
  applyContentMinWidth();
9462
+ applyContentLayout();
9424
9463
  notifyHostedResize();
9425
9464
  };
9426
9465
  const moveBy = (dx, dy) => {
@@ -9521,7 +9560,6 @@ function ItemToolBar($$anchor, $$props) {
9521
9560
  element2.style.height = "100%";
9522
9561
  element2.style.flex = "1 1 auto";
9523
9562
  element2.style.minHeight = "0";
9524
- applyContentMinWidth();
9525
9563
  if (mountedContentElement && mountedContentElement !== element2) {
9526
9564
  if (mountedContentElement.parentNode === contentEl) {
9527
9565
  invokeElementUnmount(mountedContentElement);
@@ -9536,6 +9574,8 @@ function ItemToolBar($$anchor, $$props) {
9536
9574
  contentEl.appendChild(element2);
9537
9575
  }
9538
9576
  mountedContentElement = element2;
9577
+ applyContentMinWidth();
9578
+ applyContentLayout();
9539
9579
  notifyHostedMount(element2);
9540
9580
  };
9541
9581
  const bringToFront = () => {
@@ -9805,6 +9845,7 @@ function ItemToolBar($$anchor, $$props) {
9805
9845
  }
9806
9846
  applyShellStyle();
9807
9847
  applyContentMinWidth();
9848
+ applyContentLayout();
9808
9849
  notifyHostedResize();
9809
9850
  }
9810
9851
  };
@@ -10017,7 +10058,8 @@ function ItemToolBar($$anchor, $$props) {
10017
10058
  contentEl.style.width = "100%";
10018
10059
  contentEl.style.flex = "1 1 auto";
10019
10060
  contentEl.style.minHeight = "0";
10020
- contentEl.style.overflow = "hidden";
10061
+ contentEl.style.overflowX = "hidden";
10062
+ contentEl.style.overflowY = getContentOverflowY();
10021
10063
  contentEl.style.borderRadius = "0 0 12px 12px";
10022
10064
  if (isCalculatorShell) {
10023
10065
  const makeFocusGuard = (label) => {
@@ -10135,8 +10177,8 @@ function ItemToolBar($$anchor, $$props) {
10135
10177
  titleEl.textContent = currentArgs.mounted.entry.shell?.title || currentArgs.mounted.toolId;
10136
10178
  const closeButtonOpenDisplay = currentArgs.mounted.toolId === "calculator" ? "inline-block" : "inline-flex";
10137
10179
  closeButtonEl.style.display = currentArgs.mounted.entry.shell?.closeable === false ? "none" : closeButtonOpenDisplay;
10138
- mountContent();
10139
10180
  applyShellStyle();
10181
+ mountContent();
10140
10182
  notifyHostedResize();
10141
10183
  if (!previousActive && currentArgs.active) {
10142
10184
  installFocusTrap();
@@ -18666,7 +18666,11 @@ var calculatorToolRegistration = {
18666
18666
  minWidth: 380,
18667
18667
  minHeight: 420,
18668
18668
  initialAlign: "bottom-right",
18669
- initialMargin: 16
18669
+ initialMargin: 16,
18670
+ content: {
18671
+ overflowY: "auto",
18672
+ preserveMinHeight: true
18673
+ }
18670
18674
  }
18671
18675
  }
18672
18676
  ],
@@ -18971,12 +18975,16 @@ var isServerBackend = (backend) => backend === "polly" || backend === "google" |
18971
18975
  var withDefault = (value, fallback2) => value === undefined ? fallback2 : value;
18972
18976
  var normalizeTTSLayoutMode = (value, fallback2 = "left-aligned") => typeof value === "string" && VALID_TTS_LAYOUT_MODES.has(value) ? value : fallback2;
18973
18977
  var DEFAULT_TTS_SPEED_OPTIONS = Object.freeze([0.8, 1.25]);
18974
- var normalizeSpeedRate = (entry) => {
18978
+ var DEFAULT_TTS_SPEED_CONTROL_OPTIONS = Object.freeze([
18979
+ { rate: 0.8, label: "Slow", ariaLabel: "Slow speed" },
18980
+ { rate: 1, label: "Normal", ariaLabel: "Normal speed", default: true },
18981
+ { rate: 1.25, label: "Fast", ariaLabel: "Fast speed" }
18982
+ ]);
18983
+ var normalizeControlSpeedRate = (entry) => {
18975
18984
  if (typeof entry !== "number" || !Number.isFinite(entry) || entry <= 0) {
18976
18985
  return;
18977
18986
  }
18978
- const rounded = Math.round(entry * 100) / 100;
18979
- return rounded === 1 ? undefined : rounded;
18987
+ return Math.round(entry * 100) / 100;
18980
18988
  };
18981
18989
  var trimOptionalText = (value) => {
18982
18990
  if (typeof value !== "string")
@@ -18985,7 +18993,7 @@ var trimOptionalText = (value) => {
18985
18993
  return trimmed.length ? trimmed : undefined;
18986
18994
  };
18987
18995
  var formatSpeedLabel = (rate) => `${rate}x`;
18988
- var formatSpeedAriaLabel = (label, usedDefaultLabel) => usedDefaultLabel ? `Speed ${label}` : `${label} speed`;
18996
+ var formatSpeedAriaLabel = (label, usedDefaultLabel) => label.toLowerCase() === "normal" ? "Normal speed" : usedDefaultLabel ? `Speed ${label}` : `${label} speed`;
18989
18997
  var normalizeSpeedAriaLabel = (label, ariaLabel, usedDefaultLabel) => {
18990
18998
  if (!ariaLabel)
18991
18999
  return formatSpeedAriaLabel(label, usedDefaultLabel);
@@ -18993,59 +19001,55 @@ var normalizeSpeedAriaLabel = (label, ariaLabel, usedDefaultLabel) => {
18993
19001
  return ariaLabel;
18994
19002
  return `${label} ${ariaLabel}`;
18995
19003
  };
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) => {
19014
- if (!Array.isArray(value))
19015
- return [...DEFAULT_TTS_SPEED_OPTIONS];
19016
- if (value.length === 0)
19004
+ var normalizeTTSSpeedControlOptions = (value) => {
19005
+ const input = Array.isArray(value) ? value : [...DEFAULT_TTS_SPEED_CONTROL_OPTIONS];
19006
+ if (input.length === 0)
19017
19007
  return [];
19018
19008
  const dedupedRates = new Set;
19019
19009
  const normalized = [];
19020
- for (const entry of value) {
19021
- const option = normalizeTTSSpeedOptionConfig(entry);
19022
- if (option === undefined)
19023
- continue;
19024
- const rate = typeof option === "number" ? option : option.rate;
19025
- if (dedupedRates.has(rate))
19010
+ for (const entry of input) {
19011
+ const record = toRecord(entry);
19012
+ const rate = typeof entry === "number" ? normalizeControlSpeedRate(entry) : normalizeControlSpeedRate(record.rate);
19013
+ if (rate === undefined || dedupedRates.has(rate))
19026
19014
  continue;
19027
19015
  dedupedRates.add(rate);
19028
- normalized.push(option);
19016
+ const defaultLabel = rate === 1 ? "Normal" : typeof entry === "number" ? formatSpeedLabel(rate) : formatSpeedLabel(rate);
19017
+ const label = typeof entry === "number" ? defaultLabel : trimOptionalText(record.label) || defaultLabel;
19018
+ const ariaLabel = typeof entry === "number" ? rate === 1 ? "Normal speed" : formatSpeedAriaLabel(label, true) : normalizeSpeedAriaLabel(label, trimOptionalText(record.ariaLabel), label === defaultLabel);
19019
+ normalized.push({
19020
+ rate,
19021
+ label,
19022
+ ariaLabel,
19023
+ isDefault: false,
19024
+ requestedDefault: record.default === true || record.isDefault === true
19025
+ });
19029
19026
  }
19030
- return normalized.length ? normalized : [...DEFAULT_TTS_SPEED_OPTIONS];
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)
19027
+ if (!normalized.length) {
19028
+ return normalizeTTSSpeedControlOptions(DEFAULT_TTS_SPEED_CONTROL_OPTIONS);
19029
+ }
19030
+ if (!dedupedRates.has(1)) {
19031
+ const normalOption = {
19032
+ rate: 1,
19033
+ label: "Normal",
19034
+ ariaLabel: "Normal speed",
19035
+ isDefault: false,
19036
+ requestedDefault: false
19039
19037
  };
19038
+ const firstFasterIndex = normalized.findIndex((option) => option.rate > 1);
19039
+ if (firstFasterIndex >= 0) {
19040
+ normalized.splice(firstFasterIndex, 0, normalOption);
19041
+ } else {
19042
+ normalized.push(normalOption);
19043
+ }
19040
19044
  }
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
- });
19045
+ const requestedDefaultIndex = normalized.findIndex((option) => option.requestedDefault);
19046
+ const defaultIndex = requestedDefaultIndex >= 0 ? requestedDefaultIndex : normalized.findIndex((option) => option.rate === 1);
19047
+ return normalized.map(({ requestedDefault: _requestedDefault, ...option }, index2) => ({
19048
+ ...option,
19049
+ isDefault: index2 === defaultIndex
19050
+ }));
19051
+ };
19052
+ var DEFAULT_TTS_SPEED_CONTROL_RATES = Object.freeze([0.8, 1, 1.25]);
19049
19053
  var applyRuntimeDefaults = (config) => {
19050
19054
  const withLayoutDefaults = {
19051
19055
  ...config,
@@ -19202,7 +19206,7 @@ var ttsToolRegistration = {
19202
19206
  settings.layoutMode = normalizeTTSLayoutMode(settings.layoutMode);
19203
19207
  }
19204
19208
  if (settings && "speedOptions" in settings) {
19205
- settings.speedOptions = normalizeTTSSpeedOptionConfigs(settings.speedOptions);
19209
+ settings.speedOptions = normalizeTTSSpeedControlOptions(settings.speedOptions);
19206
19210
  }
19207
19211
  const normalizedConfig = {
19208
19212
  ...config
@@ -19211,7 +19215,7 @@ var ttsToolRegistration = {
19211
19215
  normalizedConfig.layoutMode = normalizeTTSLayoutMode(normalizedConfig.layoutMode);
19212
19216
  }
19213
19217
  if ("speedOptions" in normalizedConfig) {
19214
- normalizedConfig.speedOptions = normalizeTTSSpeedOptionConfigs(normalizedConfig.speedOptions);
19218
+ normalizedConfig.speedOptions = normalizeTTSSpeedControlOptions(normalizedConfig.speedOptions);
19215
19219
  }
19216
19220
  if (settings) {
19217
19221
  normalizedConfig.settings = settings;
@@ -19264,6 +19268,7 @@ var ttsToolRegistration = {
19264
19268
  element2.setAttribute("size", resolveControlSize());
19265
19269
  element2.setAttribute("layout-mode", resolveLayoutMode());
19266
19270
  element2.speedOptions = resolveElementSpeedOptions();
19271
+ element2.showSingleSpeedOption = resolveRuntimeSettings().showSingleSpeedOption === true;
19267
19272
  return element2;
19268
19273
  };
19269
19274
  const hostLayout = resolveHostLayout();
@@ -19301,6 +19306,7 @@ var ttsToolRegistration = {
19301
19306
  element2.setAttribute("size", resolveControlSize());
19302
19307
  element2.setAttribute("layout-mode", resolveLayoutMode());
19303
19308
  element2.speedOptions = resolveElementSpeedOptions();
19309
+ element2.showSingleSpeedOption = resolveRuntimeSettings().showSingleSpeedOption === true;
19304
19310
  }
19305
19311
  };
19306
19312
  }
@@ -23911,6 +23917,7 @@ class TTSService {
23911
23917
  currentBoundaryOffset = 0;
23912
23918
  activeWordBoundaryOffset = 0;
23913
23919
  seekSegments = [];
23920
+ playbackChunks = [];
23914
23921
  sentenceHighlightSegments = [];
23915
23922
  currentSeekSegmentIndex = 0;
23916
23923
  activeSentenceStartOffset = null;
@@ -24409,6 +24416,20 @@ class TTSService {
24409
24416
  const structuralBoundaries = args.playbackSegments.map((segment) => segment.startOffset).filter((offset) => offset > 0);
24410
24417
  return this.splitSegmentsAtBoundaries(grammaticalSegments, structuralBoundaries, args.contentToSpeak);
24411
24418
  }
24419
+ createSeekSegmentsFromSpeechChunks(chunks) {
24420
+ let offset = 0;
24421
+ return chunks.map((chunk) => {
24422
+ const speechText = chunk.plainFallback?.speechText || (this.hasExplicitBreakSemantics(chunk.speechText) ? chunk.visibleText : chunk.speechText) || chunk.visibleText;
24423
+ const text2 = normalizeTextForSpeech(speechText) || chunk.visibleText || "";
24424
+ const segment = {
24425
+ text: text2,
24426
+ startOffset: offset,
24427
+ pauseMsAfter: 0
24428
+ };
24429
+ offset += Math.max(1, text2.length + 1);
24430
+ return segment;
24431
+ });
24432
+ }
24412
24433
  getCurrentSeekSegmentIndex() {
24413
24434
  if (this.seekSegments.length === 0)
24414
24435
  return 0;
@@ -24572,7 +24593,8 @@ class TTSService {
24572
24593
  this.activeWordBoundaryOffset = options?.wordBoundaryOffset || 0;
24573
24594
  const hasExplicitBreaks = this.hasExplicitBreakSemantics(contentToSpeak);
24574
24595
  const shouldUsePlan = !!this.currentContentElement && !usedCatalogSpoken && !hasExplicitBreaks && speechMatchesVisibleText;
24575
- this.seekSegments = hasExplicitBreaks || !speechMatchesVisibleText ? [] : shouldUsePlan && this.currentContentElement ? this.createSpeechPlan(this.currentContentElement, normalizedText) : this.createSeekSegmentsFromText(highlightText);
24596
+ this.playbackChunks = speechChunks?.length ? speechChunks : [];
24597
+ this.seekSegments = this.playbackChunks.length ? this.createSeekSegmentsFromSpeechChunks(this.playbackChunks) : hasExplicitBreaks || !speechMatchesVisibleText ? [] : shouldUsePlan && this.currentContentElement ? this.createSpeechPlan(this.currentContentElement, normalizedText) : this.createSeekSegmentsFromText(highlightText);
24576
24598
  this.sentenceHighlightSegments = hasExplicitBreaks ? [] : this.createSentenceHighlightSegments({
24577
24599
  contentToSpeak: highlightText,
24578
24600
  shouldUsePlan,
@@ -24929,6 +24951,7 @@ class TTSService {
24929
24951
  this.currentContentElement = options?.contentElement || null;
24930
24952
  this.lastError = null;
24931
24953
  this.currentBoundaryOffset = 0;
24954
+ this.playbackChunks = [];
24932
24955
  this.currentSeekSegmentIndex = 0;
24933
24956
  this.activeSentenceStartOffset = null;
24934
24957
  }
@@ -25138,9 +25161,15 @@ class TTSService {
25138
25161
  }
25139
25162
  async executeSpeakPlayback(args) {
25140
25163
  if (args.speechChunks?.length && this.provider) {
25141
- for (const chunk of args.speechChunks) {
25164
+ for (let index2 = 0;index2 < args.speechChunks.length; index2++) {
25142
25165
  if (args.runId !== this.speakRunId)
25143
25166
  return;
25167
+ this.currentSeekSegmentIndex = index2;
25168
+ const segment = this.seekSegments[index2];
25169
+ if (segment) {
25170
+ this.currentBoundaryOffset = segment.startOffset;
25171
+ }
25172
+ const chunk = args.speechChunks[index2];
25144
25173
  await this.speakCatalogChunk(chunk, args.runId);
25145
25174
  }
25146
25175
  return;
@@ -25170,6 +25199,7 @@ class TTSService {
25170
25199
  this.currentBoundaryOffset = 0;
25171
25200
  this.activeWordBoundaryOffset = 0;
25172
25201
  this.seekSegments = [];
25202
+ this.playbackChunks = [];
25173
25203
  this.sentenceHighlightSegments = [];
25174
25204
  this.currentSeekSegmentIndex = 0;
25175
25205
  this.activeSentenceStartOffset = null;
@@ -25232,6 +25262,7 @@ class TTSService {
25232
25262
  if (!this.provider || this.seekSegments.length === 0)
25233
25263
  return;
25234
25264
  const safeTargetIndex = Math.max(0, Math.min(this.seekSegments.length - 1, targetIndex));
25265
+ const restartChunks = this.playbackChunks.length ? this.playbackChunks.slice(safeTargetIndex) : null;
25235
25266
  this.speakRunId += 1;
25236
25267
  this.provider.onWordBoundary = undefined;
25237
25268
  this.provider.stop();
@@ -25244,9 +25275,23 @@ class TTSService {
25244
25275
  highlightMode: this.activeHighlightMode,
25245
25276
  wordBoundaryOffset: this.activeWordBoundaryOffset
25246
25277
  });
25247
- await this.speakWithPlan(restartSegments, runId, {
25248
- highlightMode: this.activeHighlightMode
25249
- });
25278
+ if (restartChunks) {
25279
+ for (let index2 = 0;index2 < restartChunks.length; index2++) {
25280
+ if (runId !== this.speakRunId)
25281
+ return;
25282
+ const absoluteIndex = safeTargetIndex + index2;
25283
+ this.currentSeekSegmentIndex = absoluteIndex;
25284
+ const segment = this.seekSegments[absoluteIndex];
25285
+ if (segment) {
25286
+ this.currentBoundaryOffset = segment.startOffset;
25287
+ }
25288
+ await this.speakCatalogChunk(restartChunks[index2], runId);
25289
+ }
25290
+ } else {
25291
+ await this.speakWithPlan(restartSegments, runId, {
25292
+ highlightMode: this.activeHighlightMode
25293
+ });
25294
+ }
25250
25295
  if (runId !== this.speakRunId)
25251
25296
  return;
25252
25297
  this.setState(PlaybackState.IDLE);
@@ -25315,6 +25360,7 @@ class TTSService {
25315
25360
  this.currentBoundaryOffset = 0;
25316
25361
  this.activeWordBoundaryOffset = 0;
25317
25362
  this.seekSegments = [];
25363
+ this.playbackChunks = [];
25318
25364
  this.sentenceHighlightSegments = [];
25319
25365
  this.currentSeekSegmentIndex = 0;
25320
25366
  this.activeSentenceStartOffset = null;