@pie-players/pie-assessment-toolkit 0.3.64 → 0.3.66

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 (74) hide show
  1. package/README.md +81 -33
  2. package/dist/components/ItemToolBar.custom-element.js +1 -1
  3. package/dist/components/PieAssessmentToolkit.custom-element.js +9 -8
  4. package/dist/components/SectionToolBar.custom-element.js +1 -1
  5. package/dist/components/chunks/ItemToolBar-8jgdz50p.js +51 -0
  6. package/dist/components/chunks/ItemToolBar-cvs646j3.js +36 -0
  7. package/dist/index.d.ts +7 -8
  8. package/dist/index.js +10 -5
  9. package/dist/policy/core/PolicySource.d.ts +2 -2
  10. package/dist/policy/core/PolicySource.js +2 -2
  11. package/dist/policy/core/ToolPolicyEngine.d.ts +7 -3
  12. package/dist/policy/core/ToolPolicyEngine.js +11 -3
  13. package/dist/policy/core/compose-decision.d.ts +3 -2
  14. package/dist/policy/core/compose-decision.js +3 -2
  15. package/dist/policy/core/decision-types.d.ts +2 -2
  16. package/dist/policy/core/decision-types.js +2 -2
  17. package/dist/policy/core/feature-decision.d.ts +28 -1
  18. package/dist/policy/core/feature-decision.js +25 -3
  19. package/dist/policy/core/pnp-policy-inputs.d.ts +2 -2
  20. package/dist/policy/core/pnp-policy-inputs.js +2 -2
  21. package/dist/policy/core/provenance.d.ts +4 -1
  22. package/dist/policy/core/provenance.js +4 -1
  23. package/dist/policy/sources/PnpPolicySource.d.ts +2 -1
  24. package/dist/policy/sources/PnpPolicySource.js +2 -1
  25. package/dist/runtime/composition-emit-scheduler.d.ts +78 -0
  26. package/dist/runtime/composition-emit-scheduler.js +154 -0
  27. package/dist/runtime/core/engine-resolver.d.ts +1 -1
  28. package/dist/runtime/core/engine-transition.js +3 -1
  29. package/dist/services/AccessibilityCatalogResolver.d.ts +49 -10
  30. package/dist/services/AccessibilityCatalogResolver.js +180 -11
  31. package/dist/services/TTSService.d.ts +11 -0
  32. package/dist/services/TTSService.js +220 -32
  33. package/dist/services/ToolRegistry.d.ts +259 -8
  34. package/dist/services/ToolRegistry.js +181 -11
  35. package/dist/services/ToolkitCoordinator.d.ts +20 -2
  36. package/dist/services/ToolkitCoordinator.js +47 -8
  37. package/dist/services/catalog-owner.d.ts +71 -0
  38. package/dist/services/catalog-owner.js +64 -0
  39. package/dist/services/createDefaultToolRegistry.d.ts +25 -58
  40. package/dist/services/createDefaultToolRegistry.js +24 -104
  41. package/dist/services/defaultPersonalNeedsProfile.d.ts +16 -14
  42. package/dist/services/defaultPersonalNeedsProfile.js +17 -38
  43. package/dist/services/framework-error.d.ts +1 -1
  44. package/dist/services/interfaces.d.ts +15 -3
  45. package/dist/services/pnp-standard-features.d.ts +1 -1
  46. package/dist/services/tool-config-defaults.d.ts +7 -23
  47. package/dist/services/tool-config-defaults.js +7 -46
  48. package/dist/services/tool-config-validation.d.ts +1 -1
  49. package/dist/services/tool-config-validation.js +44 -4
  50. package/dist/services/tts/browser-provider.js +191 -24
  51. package/dist/services/tts-runtime-config.js +7 -2
  52. package/dist/tools/internal.d.ts +35 -0
  53. package/dist/tools/internal.js +33 -0
  54. package/dist/tools/tool-tag-map.d.ts +15 -3
  55. package/dist/tools/tool-tag-map.js +21 -18
  56. package/package.json +14 -10
  57. package/dist/components/chunks/ItemToolBar-3cppre9r.js +0 -51
  58. package/dist/components/chunks/ItemToolBar-7rq2gj8b.js +0 -22
  59. package/dist/runtime/catalog-registration.d.ts +0 -67
  60. package/dist/runtime/catalog-registration.js +0 -86
  61. package/dist/services/sign-language-cards.d.ts +0 -82
  62. package/dist/services/sign-language-cards.js +0 -133
  63. package/dist/tools/registrations/accessibility-tools.d.ts +0 -34
  64. package/dist/tools/registrations/accessibility-tools.js +0 -217
  65. package/dist/tools/registrations/calculator.d.ts +0 -20
  66. package/dist/tools/registrations/calculator.js +0 -228
  67. package/dist/tools/registrations/interaction-tools.d.ts +0 -27
  68. package/dist/tools/registrations/interaction-tools.js +0 -143
  69. package/dist/tools/registrations/measurement-tools.d.ts +0 -24
  70. package/dist/tools/registrations/measurement-tools.js +0 -130
  71. package/dist/tools/registrations/subject-specific-tools.d.ts +0 -27
  72. package/dist/tools/registrations/subject-specific-tools.js +0 -158
  73. package/dist/tools/registrations/tts.d.ts +0 -21
  74. package/dist/tools/registrations/tts.js +0 -184
@@ -97,6 +97,13 @@ export class TTSService {
97
97
  sentenceHighlightSegments = [];
98
98
  currentSeekSegmentIndex = 0;
99
99
  activeSentenceStartOffset = null;
100
+ playbackStartDeferredRunId = null;
101
+ pendingPlaybackStartHighlights = [];
102
+ playbackStartBarrierCleanup = null;
103
+ playbackRateWriteQueue = Promise.resolve();
104
+ playbackRateRequestId = 0;
105
+ pendingPlaybackRateRequest = null;
106
+ activePlaybackRate = null;
100
107
  activeHighlightMode = "word";
101
108
  lastRenderedRegionTarget = null;
102
109
  telemetryReporter = null;
@@ -130,6 +137,61 @@ export class TTSService {
130
137
  console.debug(message, detail);
131
138
  }
132
139
  }
140
+ clearPlaybackStartBarrier(cleanup = this.playbackStartBarrierCleanup) {
141
+ cleanup?.();
142
+ if (this.playbackStartBarrierCleanup === cleanup) {
143
+ this.playbackStartBarrierCleanup = null;
144
+ }
145
+ }
146
+ notifyPlaybackStarted(runId) {
147
+ if (this.playbackStartDeferredRunId !== runId ||
148
+ runId !== this.speakRunId) {
149
+ return;
150
+ }
151
+ this.setState(PlaybackState.PLAYING);
152
+ const pendingHighlights = this.pendingPlaybackStartHighlights.splice(0);
153
+ for (const applyHighlight of pendingHighlights) {
154
+ applyHighlight();
155
+ }
156
+ }
157
+ installPlaybackStartBarrier(runId, hasMediaStartSignal = false) {
158
+ this.clearPlaybackStartBarrier();
159
+ const provider = this.provider;
160
+ const startAwareProvider = provider && "onPlaybackStart" in provider ? provider : null;
161
+ if (!startAwareProvider && !hasMediaStartSignal)
162
+ return null;
163
+ const previousOnPlaybackStart = startAwareProvider?.onPlaybackStart;
164
+ const onPlaybackStart = () => {
165
+ try {
166
+ this.notifyPlaybackStarted(runId);
167
+ }
168
+ finally {
169
+ previousOnPlaybackStart?.();
170
+ }
171
+ };
172
+ if (startAwareProvider) {
173
+ startAwareProvider.onPlaybackStart = onPlaybackStart;
174
+ }
175
+ this.playbackStartDeferredRunId = runId;
176
+ const cleanup = () => {
177
+ if (startAwareProvider?.onPlaybackStart === onPlaybackStart) {
178
+ startAwareProvider.onPlaybackStart = previousOnPlaybackStart;
179
+ }
180
+ if (this.playbackStartDeferredRunId === runId) {
181
+ this.playbackStartDeferredRunId = null;
182
+ this.pendingPlaybackStartHighlights = [];
183
+ }
184
+ };
185
+ this.playbackStartBarrierCleanup = cleanup;
186
+ return cleanup;
187
+ }
188
+ runWhenPlaybackStarts(runId, callback) {
189
+ if (this.playbackStartDeferredRunId === runId) {
190
+ this.pendingPlaybackStartHighlights.push(callback);
191
+ return;
192
+ }
193
+ callback();
194
+ }
133
195
  async emitTelemetry(eventName, payload) {
134
196
  try {
135
197
  await this.telemetryReporter?.(eventName, payload);
@@ -810,7 +872,9 @@ export class TTSService {
810
872
  }
811
873
  this.currentBoundaryOffset = segment.startOffset;
812
874
  if (shouldTrackSentenceProgress) {
813
- this.highlightSentenceSegment(segment.startOffset, segment.text);
875
+ this.runWhenPlaybackStarts(runId, () => {
876
+ this.highlightSentenceSegment(segment.startOffset, segment.text);
877
+ });
814
878
  }
815
879
  await this.provider.speak(segment.text);
816
880
  const pauseMs = segment.pauseMsAfter ?? 0;
@@ -923,6 +987,8 @@ export class TTSService {
923
987
  throw new Error("TTS service not initialized");
924
988
  }
925
989
  const runId = ++this.speakRunId;
990
+ this.clearPlaybackStartBarrier();
991
+ let playbackStartBarrier = null;
926
992
  try {
927
993
  await this.applyLanguageSettings(options);
928
994
  if (runId !== this.speakRunId)
@@ -962,11 +1028,13 @@ export class TTSService {
962
1028
  playbackSegments: this.seekSegments,
963
1029
  });
964
1030
  this.setState(PlaybackState.LOADING);
1031
+ playbackStartBarrier = this.installPlaybackStartBarrier(runId, !!this.playbackChunks[0]?.audio);
965
1032
  this.prepareHighlightsForSpeak({
966
1033
  contentToSpeak: highlightText,
967
1034
  options,
968
1035
  highlightMode,
969
1036
  shouldUsePlan,
1037
+ runId,
970
1038
  });
971
1039
  if (speechMatchesVisibleText) {
972
1040
  this.configureWordBoundaryHighlighting({
@@ -977,7 +1045,10 @@ export class TTSService {
977
1045
  else {
978
1046
  this.clearWordBoundaryHighlighting();
979
1047
  }
980
- this.setState(PlaybackState.PLAYING);
1048
+ if (!playbackStartBarrier) {
1049
+ this.setState(PlaybackState.PLAYING);
1050
+ }
1051
+ this.activePlaybackRate = this.normalizePlaybackRate(Number(this.ttsConfig.rate ?? 1));
981
1052
  await this.executeSpeakPlayback({
982
1053
  shouldUsePlan,
983
1054
  runId,
@@ -1001,13 +1072,30 @@ export class TTSService {
1001
1072
  this.clearHighlightsAndTracking();
1002
1073
  throw finalError;
1003
1074
  }
1075
+ finally {
1076
+ this.clearPlaybackStartBarrier(playbackStartBarrier);
1077
+ }
1004
1078
  }
1005
1079
  async switchProviderToBrowser(reason, context) {
1006
1080
  if (!this.currentProvider)
1007
1081
  return false;
1008
1082
  const fallbackProvider = new BrowserTTSProvider();
1009
1083
  const previousProviderId = this.currentProvider.providerId;
1010
- const browserConfig = { ...this.ttsConfig };
1084
+ // Voice identifiers and provider extensions belong to the provider that
1085
+ // rejected initialization. Carry only settings whose meaning is portable
1086
+ // when crossing the provider boundary; otherwise a Polly/Google/SC voice
1087
+ // such as "Joanna" can incorrectly become an explicit Browser voice.
1088
+ const browserConfig = {};
1089
+ if (this.ttsConfig.rate !== undefined) {
1090
+ browserConfig.rate = this.ttsConfig.rate;
1091
+ }
1092
+ if (this.ttsConfig.pitch !== undefined) {
1093
+ browserConfig.pitch = this.ttsConfig.pitch;
1094
+ }
1095
+ if (this.ttsConfig.mathTokenHighlighting !== undefined) {
1096
+ browserConfig.mathTokenHighlighting =
1097
+ this.ttsConfig.mathTokenHighlighting;
1098
+ }
1011
1099
  const operation = context?.operation || "tts-initialize";
1012
1100
  const offendingText = context?.contentToSpeak ?? "";
1013
1101
  const offendingPreview = offendingText
@@ -1431,20 +1519,22 @@ export class TTSService {
1431
1519
  return;
1432
1520
  }
1433
1521
  const initialSegment = this.sentenceHighlightSegments[0] || this.seekSegments[0];
1434
- if (initialSegment) {
1435
- this.highlightSentenceSegment(initialSegment.startOffset, initialSegment.text);
1436
- this.debugLog("[TTSService] Applied initial sentence highlighting");
1437
- }
1438
- else {
1439
- try {
1440
- const range = document.createRange();
1441
- range.selectNodeContents(this.currentContentElement);
1442
- this.paintTTSSentenceRanges([range]);
1522
+ this.runWhenPlaybackStarts(args.runId, () => {
1523
+ if (initialSegment) {
1524
+ this.highlightSentenceSegment(initialSegment.startOffset, initialSegment.text);
1525
+ this.debugLog("[TTSService] Applied initial sentence highlighting");
1443
1526
  }
1444
- catch {
1445
- // No-op fallback when DOM range creation fails.
1527
+ else {
1528
+ try {
1529
+ const range = document.createRange();
1530
+ range.selectNodeContents(this.currentContentElement);
1531
+ this.paintTTSSentenceRanges([range]);
1532
+ }
1533
+ catch {
1534
+ // No-op fallback when DOM range creation fails.
1535
+ }
1446
1536
  }
1447
- }
1537
+ });
1448
1538
  }
1449
1539
  configureWordBoundaryHighlighting(args) {
1450
1540
  if (!this.provider ||
@@ -1605,6 +1695,16 @@ export class TTSService {
1605
1695
  // fallback, so this cannot recurse.
1606
1696
  if (chunk.plainFallback && runId === this.speakRunId) {
1607
1697
  this.debugLog("[TTSService] SSML chunk speak failed; retrying plain text", { message: error instanceof Error ? error.message : String(error) });
1698
+ // A recorded-first run can own the start barrier even when its fallback
1699
+ // provider has no formal start signal. Drop the failed recording's queued
1700
+ // highlight and resume that non-start-aware provider's immediate-start lifecycle.
1701
+ if (this.playbackStartDeferredRunId === runId &&
1702
+ this.provider &&
1703
+ !("onPlaybackStart" in this.provider)) {
1704
+ this.pendingPlaybackStartHighlights = [];
1705
+ this.clearPlaybackStartBarrier();
1706
+ this.setState(PlaybackState.PLAYING);
1707
+ }
1608
1708
  await this.speakCatalogChunkOnce(chunk.plainFallback, runId);
1609
1709
  return;
1610
1710
  }
@@ -1621,7 +1721,7 @@ export class TTSService {
1621
1721
  * would highlight the wrong words confidently rather than the right region
1622
1722
  * vaguely.
1623
1723
  */
1624
- async playRecordedAudio(media) {
1724
+ async playRecordedAudio(media, onPlaybackStart) {
1625
1725
  if (typeof document === "undefined") {
1626
1726
  throw new Error("[tts] no document available to play recorded audio");
1627
1727
  }
@@ -1632,9 +1732,18 @@ export class TTSService {
1632
1732
  element.playbackRate = this.normalizePlaybackRate(Number(this.ttsConfig.rate || 1));
1633
1733
  await new Promise((resolve, reject) => {
1634
1734
  let endGuard;
1735
+ let didStart = false;
1736
+ let settled = false;
1737
+ const markStarted = () => {
1738
+ if (settled || didStart)
1739
+ return;
1740
+ didStart = true;
1741
+ onPlaybackStart?.();
1742
+ };
1635
1743
  const cleanup = () => {
1636
1744
  element.removeEventListener("ended", onEnded);
1637
1745
  element.removeEventListener("error", onError);
1746
+ element.removeEventListener("playing", markStarted);
1638
1747
  if (endGuard !== undefined)
1639
1748
  clearInterval(endGuard);
1640
1749
  if (this.activeRecordedAudio?.element === element) {
@@ -1642,10 +1751,16 @@ export class TTSService {
1642
1751
  }
1643
1752
  };
1644
1753
  const onEnded = () => {
1754
+ if (settled)
1755
+ return;
1756
+ settled = true;
1645
1757
  cleanup();
1646
1758
  resolve();
1647
1759
  };
1648
1760
  const onError = () => {
1761
+ if (settled)
1762
+ return;
1763
+ settled = true;
1649
1764
  cleanup();
1650
1765
  reject(new Error(`[tts] recorded audio failed to play: ${source.src}`));
1651
1766
  };
@@ -1655,6 +1770,9 @@ export class TTSService {
1655
1770
  this.activeRecordedAudio = {
1656
1771
  element,
1657
1772
  cancel: () => {
1773
+ if (settled)
1774
+ return;
1775
+ settled = true;
1658
1776
  cleanup();
1659
1777
  element.pause();
1660
1778
  resolve();
@@ -1662,6 +1780,7 @@ export class TTSService {
1662
1780
  };
1663
1781
  element.addEventListener("ended", onEnded);
1664
1782
  element.addEventListener("error", onError);
1783
+ element.addEventListener("playing", markStarted);
1665
1784
  // Browsers honour a Media Fragments start offset but are inconsistent
1666
1785
  // about the end bound, so the end is enforced here — the same reason the
1667
1786
  // signing region enforces its own.
@@ -1672,7 +1791,7 @@ export class TTSService {
1672
1791
  onEnded();
1673
1792
  }, 100);
1674
1793
  }
1675
- Promise.resolve(element.play()).catch(onError);
1794
+ Promise.resolve(element.play()).then(markStarted).catch(onError);
1676
1795
  }).finally(() => {
1677
1796
  if (this.activeRecordedAudio?.element === element) {
1678
1797
  this.activeRecordedAudio = null;
@@ -1693,8 +1812,12 @@ export class TTSService {
1693
1812
  return;
1694
1813
  this.lastRenderedRegionTarget = null;
1695
1814
  if (chunk.audio) {
1696
- this.highlightCatalogRegion(chunk);
1697
- await this.playRecordedAudio(chunk.audio);
1815
+ this.runWhenPlaybackStarts(runId, () => {
1816
+ this.highlightCatalogRegion(chunk);
1817
+ });
1818
+ await this.playRecordedAudio(chunk.audio, () => {
1819
+ this.notifyPlaybackStarted(runId);
1820
+ });
1698
1821
  return;
1699
1822
  }
1700
1823
  const contentRoot = chunk.regionElement || chunk.sourceElement || this.currentContentElement;
@@ -1713,12 +1836,14 @@ export class TTSService {
1713
1836
  mathTokenHighlighting,
1714
1837
  })
1715
1838
  : null;
1716
- if (highlightPlan && pipelineChunk && chunk.mathAlignment) {
1717
- this.renderHighlightDecision(highlightPlan.resolveInitial(pipelineChunk.id));
1718
- }
1719
- else {
1720
- this.highlightCatalogRegion(chunk);
1721
- }
1839
+ this.runWhenPlaybackStarts(runId, () => {
1840
+ if (highlightPlan && pipelineChunk && chunk.mathAlignment) {
1841
+ this.renderHighlightDecision(highlightPlan.resolveInitial(pipelineChunk.id));
1842
+ }
1843
+ else {
1844
+ this.highlightCatalogRegion(chunk);
1845
+ }
1846
+ });
1722
1847
  const canUseChunkBoundaries = chunk.sourceElement &&
1723
1848
  ((chunk.mathAlignment && chunk.mathAlignment.speech.tokens.length > 0) ||
1724
1849
  (chunk.mathAlignments && chunk.mathAlignments.length > 0) ||
@@ -1805,6 +1930,8 @@ export class TTSService {
1805
1930
  }
1806
1931
  }
1807
1932
  clearHighlightsAndTracking() {
1933
+ this.pendingPlaybackStartHighlights = [];
1934
+ this.activePlaybackRate = null;
1808
1935
  if (this.highlightCoordinator) {
1809
1936
  this.highlightCoordinator.clearTTS();
1810
1937
  }
@@ -1923,13 +2050,21 @@ export class TTSService {
1923
2050
  ? this.playbackChunks.slice(safeTargetIndex)
1924
2051
  : null;
1925
2052
  this.speakRunId += 1;
2053
+ this.clearPlaybackStartBarrier();
1926
2054
  this.cancelRecordedAudio();
1927
2055
  this.provider.onWordBoundary = undefined;
1928
2056
  this.provider.stop();
1929
2057
  this.currentSeekSegmentIndex = safeTargetIndex;
1930
2058
  const runId = ++this.speakRunId;
1931
2059
  const restartSegments = this.seekSegments.slice(safeTargetIndex);
1932
- this.setState(PlaybackState.PLAYING);
2060
+ this.highlightCoordinator?.clearTTS();
2061
+ this.activeSentenceStartOffset = null;
2062
+ this.setState(PlaybackState.LOADING);
2063
+ const playbackStartBarrier = this.installPlaybackStartBarrier(runId, !!restartChunks?.[0]?.audio);
2064
+ if (!playbackStartBarrier) {
2065
+ this.setState(PlaybackState.PLAYING);
2066
+ }
2067
+ this.activePlaybackRate = this.normalizePlaybackRate(Number(this.ttsConfig.rate ?? 1));
1933
2068
  try {
1934
2069
  this.configureWordBoundaryHighlighting({
1935
2070
  highlightMode: this.activeHighlightMode,
@@ -1968,17 +2103,68 @@ export class TTSService {
1968
2103
  this.highlightTargetResolverProvider = null;
1969
2104
  throw error;
1970
2105
  }
2106
+ finally {
2107
+ this.clearPlaybackStartBarrier(playbackStartBarrier);
2108
+ }
1971
2109
  }
1972
2110
  async setPlaybackRate(rate) {
2111
+ if (!this.provider) {
2112
+ throw new Error("TTSService not initialized. Call initialize() first.");
2113
+ }
1973
2114
  const nextRate = this.normalizePlaybackRate(rate);
1974
- await this.updateSettings({ rate: nextRate });
1975
- if (this.state !== PlaybackState.PLAYING ||
1976
- !this.currentText ||
1977
- this.seekSegments.length === 0 ||
1978
- this.hasExplicitBreakSemantics(this.currentText)) {
2115
+ const pendingRequest = this.pendingPlaybackRateRequest;
2116
+ if (pendingRequest?.rate === nextRate) {
2117
+ await pendingRequest.promise;
1979
2118
  return;
1980
2119
  }
1981
- await this.restartFromSeekIndex(this.getCurrentSeekSegmentIndex());
2120
+ const requestId = ++this.playbackRateRequestId;
2121
+ const settingsWrite = this.playbackRateWriteQueue.then(async () => {
2122
+ const playbackRunId = this.speakRunId;
2123
+ // A newer distinct target supersedes queued work before it mutates either
2124
+ // provider settings or playback. An already-running write cannot be
2125
+ // cancelled, so its post-write generation check still suppresses restart.
2126
+ if (requestId !== this.playbackRateRequestId) {
2127
+ return { didUpdate: false, playbackRunId };
2128
+ }
2129
+ // An omitted rate is the provider-default 1× rate. Re-check inside the
2130
+ // serialized operation so overlapping callers observe the preceding write.
2131
+ const currentRate = this.normalizePlaybackRate(Number(this.ttsConfig.rate ?? 1));
2132
+ if (nextRate === currentRate) {
2133
+ return { didUpdate: false, playbackRunId };
2134
+ }
2135
+ await this.updateSettings({ rate: nextRate });
2136
+ return { didUpdate: true, playbackRunId };
2137
+ });
2138
+ this.playbackRateWriteQueue = settingsWrite.then(() => undefined, () => undefined);
2139
+ const operation = settingsWrite.then(async (result) => {
2140
+ if (requestId !== this.playbackRateRequestId ||
2141
+ result.playbackRunId !== this.speakRunId) {
2142
+ return;
2143
+ }
2144
+ const activePlaybackNeedsRate = this.activePlaybackRate !== null &&
2145
+ this.activePlaybackRate !== nextRate;
2146
+ if (!result.didUpdate && !activePlaybackNeedsRate)
2147
+ return;
2148
+ if ((this.state !== PlaybackState.PLAYING &&
2149
+ this.state !== PlaybackState.LOADING) ||
2150
+ !this.currentText ||
2151
+ this.seekSegments.length === 0 ||
2152
+ this.hasExplicitBreakSemantics(this.currentText)) {
2153
+ return;
2154
+ }
2155
+ await this.restartFromSeekIndex(this.getCurrentSeekSegmentIndex());
2156
+ });
2157
+ let trackedOperation;
2158
+ trackedOperation = operation.finally(() => {
2159
+ if (this.pendingPlaybackRateRequest?.promise === trackedOperation) {
2160
+ this.pendingPlaybackRateRequest = null;
2161
+ }
2162
+ });
2163
+ this.pendingPlaybackRateRequest = {
2164
+ rate: nextRate,
2165
+ promise: trackedOperation,
2166
+ };
2167
+ await trackedOperation;
1982
2168
  }
1983
2169
  async seekBy(units) {
1984
2170
  if (!this.provider || !this.currentText)
@@ -2015,6 +2201,7 @@ export class TTSService {
2015
2201
  if (!this.provider)
2016
2202
  return;
2017
2203
  this.speakRunId += 1;
2204
+ this.clearPlaybackStartBarrier();
2018
2205
  this.cancelRecordedAudio();
2019
2206
  this.provider.onWordBoundary = undefined;
2020
2207
  this.provider.stop();
@@ -2036,6 +2223,7 @@ export class TTSService {
2036
2223
  this.sentenceHighlightSegments = [];
2037
2224
  this.currentSeekSegmentIndex = 0;
2038
2225
  this.activeSentenceStartOffset = null;
2226
+ this.activePlaybackRate = null;
2039
2227
  }
2040
2228
  /**
2041
2229
  * Request UI-level TTS controls to hand off/deactivate.