@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.
- package/README.md +81 -33
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +9 -8
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-8jgdz50p.js +51 -0
- package/dist/components/chunks/ItemToolBar-cvs646j3.js +36 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.js +10 -5
- package/dist/policy/core/PolicySource.d.ts +2 -2
- package/dist/policy/core/PolicySource.js +2 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +7 -3
- package/dist/policy/core/ToolPolicyEngine.js +11 -3
- package/dist/policy/core/compose-decision.d.ts +3 -2
- package/dist/policy/core/compose-decision.js +3 -2
- package/dist/policy/core/decision-types.d.ts +2 -2
- package/dist/policy/core/decision-types.js +2 -2
- package/dist/policy/core/feature-decision.d.ts +28 -1
- package/dist/policy/core/feature-decision.js +25 -3
- package/dist/policy/core/pnp-policy-inputs.d.ts +2 -2
- package/dist/policy/core/pnp-policy-inputs.js +2 -2
- package/dist/policy/core/provenance.d.ts +4 -1
- package/dist/policy/core/provenance.js +4 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +2 -1
- package/dist/policy/sources/PnpPolicySource.js +2 -1
- package/dist/runtime/composition-emit-scheduler.d.ts +78 -0
- package/dist/runtime/composition-emit-scheduler.js +154 -0
- package/dist/runtime/core/engine-resolver.d.ts +1 -1
- package/dist/runtime/core/engine-transition.js +3 -1
- package/dist/services/AccessibilityCatalogResolver.d.ts +49 -10
- package/dist/services/AccessibilityCatalogResolver.js +180 -11
- package/dist/services/TTSService.d.ts +11 -0
- package/dist/services/TTSService.js +220 -32
- package/dist/services/ToolRegistry.d.ts +259 -8
- package/dist/services/ToolRegistry.js +181 -11
- package/dist/services/ToolkitCoordinator.d.ts +20 -2
- package/dist/services/ToolkitCoordinator.js +47 -8
- package/dist/services/catalog-owner.d.ts +71 -0
- package/dist/services/catalog-owner.js +64 -0
- package/dist/services/createDefaultToolRegistry.d.ts +25 -58
- package/dist/services/createDefaultToolRegistry.js +24 -104
- package/dist/services/defaultPersonalNeedsProfile.d.ts +16 -14
- package/dist/services/defaultPersonalNeedsProfile.js +17 -38
- package/dist/services/framework-error.d.ts +1 -1
- package/dist/services/interfaces.d.ts +15 -3
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/tool-config-defaults.d.ts +7 -23
- package/dist/services/tool-config-defaults.js +7 -46
- package/dist/services/tool-config-validation.d.ts +1 -1
- package/dist/services/tool-config-validation.js +44 -4
- package/dist/services/tts/browser-provider.js +191 -24
- package/dist/services/tts-runtime-config.js +7 -2
- package/dist/tools/internal.d.ts +35 -0
- package/dist/tools/internal.js +33 -0
- package/dist/tools/tool-tag-map.d.ts +15 -3
- package/dist/tools/tool-tag-map.js +21 -18
- package/package.json +14 -10
- package/dist/components/chunks/ItemToolBar-3cppre9r.js +0 -51
- package/dist/components/chunks/ItemToolBar-7rq2gj8b.js +0 -22
- package/dist/runtime/catalog-registration.d.ts +0 -67
- package/dist/runtime/catalog-registration.js +0 -86
- package/dist/services/sign-language-cards.d.ts +0 -82
- package/dist/services/sign-language-cards.js +0 -133
- package/dist/tools/registrations/accessibility-tools.d.ts +0 -34
- package/dist/tools/registrations/accessibility-tools.js +0 -217
- package/dist/tools/registrations/calculator.d.ts +0 -20
- package/dist/tools/registrations/calculator.js +0 -228
- package/dist/tools/registrations/interaction-tools.d.ts +0 -27
- package/dist/tools/registrations/interaction-tools.js +0 -143
- package/dist/tools/registrations/measurement-tools.d.ts +0 -24
- package/dist/tools/registrations/measurement-tools.js +0 -130
- package/dist/tools/registrations/subject-specific-tools.d.ts +0 -27
- package/dist/tools/registrations/subject-specific-tools.js +0 -158
- package/dist/tools/registrations/tts.d.ts +0 -21
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
|
|
1445
|
-
|
|
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.
|
|
1697
|
-
|
|
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
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
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.
|
|
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
|
-
|
|
1975
|
-
if (
|
|
1976
|
-
|
|
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
|
-
|
|
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.
|