@pie-players/pie-assessment-toolkit 0.2.7 → 0.2.10
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 +200 -67
- package/dist/attempt/TestSession.d.ts +23 -21
- package/dist/attempt/TestSession.d.ts.map +1 -1
- package/dist/attempt/TestSession.js +17 -15
- package/dist/attempt/TestSession.js.map +1 -1
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +50 -0
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts.map +1 -0
- package/dist/attempt/adapters/activity-to-test-attempt-session.js +135 -0
- package/dist/attempt/adapters/activity-to-test-attempt-session.js.map +1 -0
- package/dist/components/ItemToolBar.custom-element.js +953 -0
- package/dist/components/PieAssessmentToolkit.custom-element.js +757 -0
- package/dist/components/SectionToolBar.custom-element.js +162 -0
- package/dist/components/item-toolbar-element.d.ts +3 -0
- package/dist/components/item-toolbar-element.d.ts.map +1 -0
- package/dist/components/item-toolbar-element.js +2 -0
- package/dist/components/item-toolbar-element.js.map +1 -0
- package/dist/components/pie-assessment-toolkit-element.d.ts +3 -0
- package/dist/components/pie-assessment-toolkit-element.d.ts.map +1 -0
- package/dist/components/pie-assessment-toolkit-element.js +2 -0
- package/dist/components/pie-assessment-toolkit-element.js.map +1 -0
- package/dist/components/section-toolbar-element.d.ts +3 -0
- package/dist/components/section-toolbar-element.d.ts.map +1 -0
- package/dist/components/section-toolbar-element.js +2 -0
- package/dist/components/section-toolbar-element.js.map +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +52 -0
- package/dist/context/assessment-toolkit-context.d.ts.map +1 -0
- package/dist/context/assessment-toolkit-context.js +6 -0
- package/dist/context/assessment-toolkit-context.js.map +1 -0
- package/dist/context/runtime-context-consumer.d.ts +14 -0
- package/dist/context/runtime-context-consumer.d.ts.map +1 -0
- package/dist/context/runtime-context-consumer.js +52 -0
- package/dist/context/runtime-context-consumer.js.map +1 -0
- package/dist/index.d.ts +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -11
- package/dist/index.js.map +1 -1
- package/dist/runtime/RuntimeRegistry.d.ts +13 -0
- package/dist/runtime/RuntimeRegistry.d.ts.map +1 -0
- package/dist/runtime/RuntimeRegistry.js +50 -0
- package/dist/runtime/RuntimeRegistry.js.map +1 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +45 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts.map +1 -0
- package/dist/runtime/SectionRuntimeEngine.js +81 -0
- package/dist/runtime/SectionRuntimeEngine.js.map +1 -0
- package/dist/runtime/registration-events.d.ts +19 -0
- package/dist/runtime/registration-events.d.ts.map +1 -0
- package/dist/runtime/registration-events.js +4 -0
- package/dist/runtime/registration-events.js.map +1 -0
- package/dist/runtime/runtime-event-guards.d.ts +3 -0
- package/dist/runtime/runtime-event-guards.d.ts.map +1 -0
- package/dist/runtime/runtime-event-guards.js +9 -0
- package/dist/runtime/runtime-event-guards.js.map +1 -0
- package/dist/runtime/tool-host-contract.d.ts +30 -0
- package/dist/runtime/tool-host-contract.d.ts.map +1 -0
- package/dist/runtime/tool-host-contract.js +48 -0
- package/dist/runtime/tool-host-contract.js.map +1 -0
- package/dist/services/HighlightCoordinator.d.ts +9 -0
- package/dist/services/HighlightCoordinator.d.ts.map +1 -1
- package/dist/services/HighlightCoordinator.js +196 -55
- package/dist/services/HighlightCoordinator.js.map +1 -1
- package/dist/services/TTSService.d.ts +21 -1
- package/dist/services/TTSService.d.ts.map +1 -1
- package/dist/services/TTSService.js +356 -117
- package/dist/services/TTSService.js.map +1 -1
- package/dist/services/ThemeProvider.d.ts +1 -0
- package/dist/services/ThemeProvider.d.ts.map +1 -1
- package/dist/services/ThemeProvider.js +82 -46
- package/dist/services/ThemeProvider.js.map +1 -1
- package/dist/services/ToolConfigResolver.d.ts +1 -1
- package/dist/services/ToolConfigResolver.js +1 -1
- package/dist/services/ToolCoordinator.d.ts +1 -1
- package/dist/services/ToolCoordinator.d.ts.map +1 -1
- package/dist/services/ToolRegistry.d.ts +76 -67
- package/dist/services/ToolRegistry.d.ts.map +1 -1
- package/dist/services/ToolRegistry.js +73 -22
- package/dist/services/ToolRegistry.js.map +1 -1
- package/dist/services/ToolkitCoordinator.d.ts +126 -8
- package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
- package/dist/services/ToolkitCoordinator.js +607 -72
- package/dist/services/ToolkitCoordinator.js.map +1 -1
- package/dist/services/createDefaultToolRegistry.d.ts +11 -12
- package/dist/services/createDefaultToolRegistry.d.ts.map +1 -1
- package/dist/services/createDefaultToolRegistry.js +9 -56
- package/dist/services/createDefaultToolRegistry.js.map +1 -1
- package/dist/services/defaultPersonalNeedsProfile.d.ts +4 -0
- package/dist/services/defaultPersonalNeedsProfile.d.ts.map +1 -0
- package/dist/services/defaultPersonalNeedsProfile.js +25 -0
- package/dist/services/defaultPersonalNeedsProfile.js.map +1 -0
- package/dist/services/interfaces.d.ts +62 -2
- package/dist/services/interfaces.d.ts.map +1 -1
- package/dist/services/interfaces.js.map +1 -1
- package/dist/services/pnp-standard-features.d.ts +3 -4
- package/dist/services/pnp-standard-features.d.ts.map +1 -1
- package/dist/services/pnp-standard-features.js +0 -3
- package/dist/services/pnp-standard-features.js.map +1 -1
- package/dist/services/section-controller-types.d.ts +37 -0
- package/dist/services/section-controller-types.d.ts.map +1 -0
- package/dist/services/section-controller-types.js +2 -0
- package/dist/services/section-controller-types.js.map +1 -0
- package/dist/services/tool-config-defaults.d.ts +14 -0
- package/dist/services/tool-config-defaults.d.ts.map +1 -0
- package/dist/services/tool-config-defaults.js +23 -0
- package/dist/services/tool-config-defaults.js.map +1 -0
- package/dist/services/tool-context.d.ts +4 -4
- package/dist/services/tool-context.d.ts.map +1 -1
- package/dist/services/tool-context.js +156 -20
- package/dist/services/tool-context.js.map +1 -1
- package/dist/services/tool-instance-id.d.ts +15 -0
- package/dist/services/tool-instance-id.d.ts.map +1 -0
- package/dist/services/tool-instance-id.js +55 -0
- package/dist/services/tool-instance-id.js.map +1 -0
- package/dist/services/tool-providers/IToolProvider.d.ts +2 -3
- package/dist/services/tool-providers/IToolProvider.d.ts.map +1 -1
- package/dist/services/tool-providers/IToolProvider.js +1 -2
- package/dist/services/tool-providers/IToolProvider.js.map +1 -1
- package/dist/services/tool-providers/MathJsToolProvider.d.ts +25 -0
- package/dist/services/tool-providers/MathJsToolProvider.d.ts.map +1 -0
- package/dist/services/tool-providers/MathJsToolProvider.js +53 -0
- package/dist/services/tool-providers/MathJsToolProvider.js.map +1 -0
- package/dist/services/tool-providers/index.d.ts +2 -0
- package/dist/services/tool-providers/index.d.ts.map +1 -1
- package/dist/services/tool-providers/index.js +1 -0
- package/dist/services/tool-providers/index.js.map +1 -1
- package/dist/services/toolbar-items.d.ts +26 -0
- package/dist/services/toolbar-items.d.ts.map +1 -0
- package/dist/services/toolbar-items.js +10 -0
- package/dist/services/toolbar-items.js.map +1 -0
- package/dist/services/tools-config-normalizer.d.ts +26 -0
- package/dist/services/tools-config-normalizer.d.ts.map +1 -0
- package/dist/services/tools-config-normalizer.js +62 -0
- package/dist/services/tools-config-normalizer.js.map +1 -0
- package/dist/services/tts/browser-provider.d.ts.map +1 -1
- package/dist/services/tts/browser-provider.js +201 -34
- package/dist/services/tts/browser-provider.js.map +1 -1
- package/dist/services/tts/text-processing.d.ts +17 -0
- package/dist/services/tts/text-processing.d.ts.map +1 -0
- package/dist/services/tts/text-processing.js +144 -0
- package/dist/services/tts/text-processing.js.map +1 -0
- package/dist/tools/calculators/ti-provider.d.ts.map +1 -1
- package/dist/tools/calculators/ti-provider.js +127 -4
- package/dist/tools/calculators/ti-provider.js.map +1 -1
- package/dist/tools/client.d.ts +2 -1
- package/dist/tools/client.d.ts.map +1 -1
- package/dist/tools/client.js +2 -1
- package/dist/tools/client.js.map +1 -1
- package/dist/tools/default-tool-module-loaders.d.ts +9 -0
- package/dist/tools/default-tool-module-loaders.d.ts.map +1 -0
- package/dist/tools/default-tool-module-loaders.js +11 -0
- package/dist/tools/default-tool-module-loaders.js.map +1 -0
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/library-loader.d.ts +1 -1
- package/dist/tools/library-loader.d.ts.map +1 -1
- package/dist/tools/library-loader.js +7 -2
- package/dist/tools/library-loader.js.map +1 -1
- package/dist/tools/registrations/accessibility-tools.d.ts +8 -11
- package/dist/tools/registrations/accessibility-tools.d.ts.map +1 -1
- package/dist/tools/registrations/accessibility-tools.js +108 -120
- package/dist/tools/registrations/accessibility-tools.js.map +1 -1
- package/dist/tools/registrations/calculator.d.ts +2 -2
- package/dist/tools/registrations/calculator.d.ts.map +1 -1
- package/dist/tools/registrations/calculator.js +42 -55
- package/dist/tools/registrations/calculator.js.map +1 -1
- package/dist/tools/registrations/interaction-tools.d.ts.map +1 -1
- package/dist/tools/registrations/interaction-tools.js +77 -52
- package/dist/tools/registrations/interaction-tools.js.map +1 -1
- package/dist/tools/registrations/measurement-tools.d.ts.map +1 -1
- package/dist/tools/registrations/measurement-tools.js +63 -38
- package/dist/tools/registrations/measurement-tools.js.map +1 -1
- package/dist/tools/registrations/subject-specific-tools.d.ts.map +1 -1
- package/dist/tools/registrations/subject-specific-tools.js +63 -38
- package/dist/tools/registrations/subject-specific-tools.js.map +1 -1
- package/dist/tools/registrations/tts.d.ts +1 -1
- package/dist/tools/registrations/tts.d.ts.map +1 -1
- package/dist/tools/registrations/tts.js +171 -41
- package/dist/tools/registrations/tts.js.map +1 -1
- package/dist/tools/response-discovery.d.ts +12 -14
- package/dist/tools/response-discovery.d.ts.map +1 -1
- package/dist/tools/response-discovery.js +23 -80
- package/dist/tools/response-discovery.js.map +1 -1
- package/dist/tools/tool-tag-map.d.ts +3 -3
- package/dist/tools/tool-tag-map.d.ts.map +1 -1
- package/dist/tools/tool-tag-map.js +4 -4
- package/dist/tools/tool-tag-map.js.map +1 -1
- package/dist/tools/types.d.ts +3 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/package.json +28 -27
- package/src/README.md +16 -32
- package/src/components/ItemToolBar.svelte +701 -0
- package/src/components/PieAssessmentToolkit.svelte +610 -0
- package/src/components/SectionToolBar.svelte +58 -0
- package/src/components/ToolButton.svelte +11 -11
- package/src/components/ToolButtonGroup.svelte +27 -11
- package/src/tools/README.md +6 -5
- package/src/tools/calculators/README.md +4 -4
- package/dist/player/AssessmentPlayer.d.ts +0 -397
- package/dist/player/AssessmentPlayer.d.ts.map +0 -1
- package/dist/player/AssessmentPlayer.js +0 -974
- package/dist/player/AssessmentPlayer.js.map +0 -1
- package/dist/player/index.d.ts +0 -17
- package/dist/player/index.d.ts.map +0 -1
- package/dist/player/index.js +0 -15
- package/dist/player/index.js.map +0 -1
- package/dist/player/navigation-types.d.ts +0 -63
- package/dist/player/navigation-types.d.ts.map +0 -1
- package/dist/player/navigation-types.js +0 -7
- package/dist/player/navigation-types.js.map +0 -1
- package/dist/player/qti-navigation.d.ts +0 -29
- package/dist/player/qti-navigation.d.ts.map +0 -1
- package/dist/player/qti-navigation.js +0 -160
- package/dist/player/qti-navigation.js.map +0 -1
- package/dist/reference-layout/index.d.ts +0 -19
- package/dist/reference-layout/index.d.ts.map +0 -1
- package/dist/reference-layout/index.js +0 -20
- package/dist/reference-layout/index.js.map +0 -1
- package/dist/services/AnnotationToolbarConfig.d.ts +0 -134
- package/dist/services/AnnotationToolbarConfig.d.ts.map +0 -1
- package/dist/services/AnnotationToolbarConfig.js +0 -100
- package/dist/services/AnnotationToolbarConfig.js.map +0 -1
- package/dist/services/AssessmentAuthoringService.d.ts +0 -60
- package/dist/services/AssessmentAuthoringService.d.ts.map +0 -1
- package/dist/services/AssessmentAuthoringService.js +0 -183
- package/dist/services/AssessmentAuthoringService.js.map +0 -1
- package/dist/services/PNPMapper.d.ts +0 -81
- package/dist/services/PNPMapper.d.ts.map +0 -1
- package/dist/services/PNPMapper.js +0 -108
- package/dist/services/PNPMapper.js.map +0 -1
- package/dist/services/tts/provider-interface.d.ts +0 -131
- package/dist/services/tts/provider-interface.d.ts.map +0 -1
- package/dist/services/tts/provider-interface.js +0 -10
- package/dist/services/tts/provider-interface.js.map +0 -1
- package/src/components/PNPProfileTester.example.svelte +0 -188
- package/src/components/PNPProfileTester.svelte +0 -397
- package/src/components/PNPProvenanceViewer.svelte +0 -260
- package/src/components/QuestionToolBar.svelte +0 -483
- package/src/player/AssessmentLayout.svelte +0 -68
- package/src/player/README.md +0 -154
- package/src/reference-layout/README.md +0 -135
- package/src/reference-layout/ReferenceLayout.svelte +0 -201
- package/src/reference-layout/components/AssessmentContent.svelte +0 -259
- package/src/reference-layout/components/AssessmentFooter.svelte +0 -89
- package/src/reference-layout/components/AssessmentHeader.svelte +0 -250
- package/src/reference-layout/components/AssessmentNavigation.svelte +0 -134
- package/src/reference-layout/components/AssessmentToolsBar.svelte +0 -176
- package/src/reference-layout/components/ItemPanel.svelte +0 -200
- package/src/reference-layout/components/NotesPanel.svelte +0 -179
- package/src/reference-layout/components/PassagePanel.svelte +0 -76
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Part of PIE Assessment Toolkit.
|
|
17
17
|
*/
|
|
18
|
+
import { collectVisibleTextAndMap, extractVisibleText as extractVisibleTextFromDOM, isElementHiddenForTTS, normalizeTextForSpeech, } from "./tts/text-processing.js";
|
|
18
19
|
/**
|
|
19
20
|
* Playback state
|
|
20
21
|
*/
|
|
@@ -38,11 +39,14 @@ export class TTSService {
|
|
|
38
39
|
highlightCoordinator = null;
|
|
39
40
|
catalogResolver = null;
|
|
40
41
|
state = PlaybackState.IDLE;
|
|
42
|
+
ttsConfig = {};
|
|
41
43
|
currentText = null;
|
|
42
44
|
currentContentElement = null;
|
|
43
45
|
normalizedToDOM = new Map();
|
|
44
46
|
listeners = new Map();
|
|
45
47
|
lastError = null;
|
|
48
|
+
speakRunId = 0;
|
|
49
|
+
currentBoundaryOffset = 0;
|
|
46
50
|
constructor() { }
|
|
47
51
|
/**
|
|
48
52
|
* Initialize TTS service with a provider
|
|
@@ -52,6 +56,7 @@ export class TTSService {
|
|
|
52
56
|
*/
|
|
53
57
|
async initialize(provider, config = {}) {
|
|
54
58
|
this.currentProvider = provider;
|
|
59
|
+
this.ttsConfig = { ...config };
|
|
55
60
|
// Initialize provider and get implementation
|
|
56
61
|
this.provider = await provider.initialize(config);
|
|
57
62
|
}
|
|
@@ -71,6 +76,7 @@ export class TTSService {
|
|
|
71
76
|
if ("updateSettings" in this.provider &&
|
|
72
77
|
typeof this.provider.updateSettings === "function") {
|
|
73
78
|
await this.provider.updateSettings(settings);
|
|
79
|
+
this.ttsConfig = { ...this.ttsConfig, ...settings };
|
|
74
80
|
}
|
|
75
81
|
else {
|
|
76
82
|
// Fallback: Reinitialize with merged config
|
|
@@ -106,15 +112,23 @@ export class TTSService {
|
|
|
106
112
|
* Build a character-by-character map from normalized text positions to DOM positions
|
|
107
113
|
* This handles the complexity of whitespace normalization
|
|
108
114
|
*/
|
|
109
|
-
|
|
115
|
+
getTextProcessingOptions(language) {
|
|
116
|
+
const providerOptions = (this.ttsConfig.providerOptions || {});
|
|
117
|
+
const textNormalization = (providerOptions.textNormalization || {});
|
|
118
|
+
const mode = textNormalization.boundarySpacingMode;
|
|
119
|
+
const boundarySpacingMode = mode === "none" || mode === "alnum" || mode === "segmenterPreferred"
|
|
120
|
+
? mode
|
|
121
|
+
: undefined;
|
|
122
|
+
const locale = language ||
|
|
123
|
+
(typeof textNormalization.locale === "string"
|
|
124
|
+
? textNormalization.locale
|
|
125
|
+
: undefined);
|
|
126
|
+
return { locale, boundarySpacingMode };
|
|
127
|
+
}
|
|
128
|
+
buildPositionMap(element, spokenText, language) {
|
|
110
129
|
this.normalizedToDOM.clear();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
range.selectNodeContents(element);
|
|
114
|
-
const domText = range.toString();
|
|
115
|
-
// Normalize the DOM text the same way the spoken text is normalized
|
|
116
|
-
// (trim + collapse whitespace)
|
|
117
|
-
const normalizedDomText = domText.trim().replace(/\s+/g, " ");
|
|
130
|
+
const { text: normalizedDomText, map } = collectVisibleTextAndMap(element, this.getTextProcessingOptions(language));
|
|
131
|
+
this.normalizedToDOM = map;
|
|
118
132
|
console.log("[TTSService] Text comparison:", {
|
|
119
133
|
spokenLength: spokenText.length,
|
|
120
134
|
normalizedDomLength: normalizedDomText.length,
|
|
@@ -138,64 +152,6 @@ export class TTSService {
|
|
|
138
152
|
return null;
|
|
139
153
|
})(),
|
|
140
154
|
});
|
|
141
|
-
// Build map by walking through nodes and tracking normalized position
|
|
142
|
-
let normalizedPos = 0;
|
|
143
|
-
let inLeadingWhitespace = true;
|
|
144
|
-
let lastCharWasWhitespace = false;
|
|
145
|
-
const walk = (node) => {
|
|
146
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
147
|
-
const textNode = node;
|
|
148
|
-
const text = textNode.textContent || "";
|
|
149
|
-
for (let i = 0; i < text.length; i++) {
|
|
150
|
-
const char = text[i];
|
|
151
|
-
const isWhitespace = /\s/.test(char);
|
|
152
|
-
if (inLeadingWhitespace) {
|
|
153
|
-
// Skip leading whitespace entirely - don't map it
|
|
154
|
-
if (!isWhitespace) {
|
|
155
|
-
inLeadingWhitespace = false;
|
|
156
|
-
this.normalizedToDOM.set(normalizedPos, {
|
|
157
|
-
node: textNode,
|
|
158
|
-
offset: i,
|
|
159
|
-
});
|
|
160
|
-
normalizedPos++;
|
|
161
|
-
lastCharWasWhitespace = false;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
// Past leading whitespace
|
|
166
|
-
if (isWhitespace) {
|
|
167
|
-
// Only map the first whitespace in a sequence (collapse multiple)
|
|
168
|
-
if (!lastCharWasWhitespace) {
|
|
169
|
-
this.normalizedToDOM.set(normalizedPos, {
|
|
170
|
-
node: textNode,
|
|
171
|
-
offset: i,
|
|
172
|
-
});
|
|
173
|
-
normalizedPos++;
|
|
174
|
-
}
|
|
175
|
-
lastCharWasWhitespace = true;
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
// Regular character - always map it
|
|
179
|
-
this.normalizedToDOM.set(normalizedPos, {
|
|
180
|
-
node: textNode,
|
|
181
|
-
offset: i,
|
|
182
|
-
});
|
|
183
|
-
normalizedPos++;
|
|
184
|
-
lastCharWasWhitespace = false;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
190
|
-
const element = node;
|
|
191
|
-
if (element.tagName !== "SCRIPT" && element.tagName !== "STYLE") {
|
|
192
|
-
for (const child of Array.from(node.childNodes)) {
|
|
193
|
-
walk(child);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
walk(element);
|
|
199
155
|
console.log("[TTSService] Position map built:", {
|
|
200
156
|
entries: this.normalizedToDOM.size,
|
|
201
157
|
spokenTextLength: spokenText.length,
|
|
@@ -211,6 +167,231 @@ export class TTSService {
|
|
|
211
167
|
})),
|
|
212
168
|
});
|
|
213
169
|
}
|
|
170
|
+
getStructuralPauseProfile() {
|
|
171
|
+
const providerOptions = (this.ttsConfig.providerOptions || {});
|
|
172
|
+
const custom = (providerOptions.structuralPauses || {});
|
|
173
|
+
const customUnits = (custom.units || {});
|
|
174
|
+
const toNumber = (value, fallback) => {
|
|
175
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
176
|
+
return value;
|
|
177
|
+
if (typeof value === "string" && value.trim()) {
|
|
178
|
+
const parsed = Number(value);
|
|
179
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
180
|
+
}
|
|
181
|
+
return fallback;
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
baseMs: toNumber(custom.baseMs, 280),
|
|
185
|
+
units: {
|
|
186
|
+
minor: toNumber(customUnits.minor, 0.65),
|
|
187
|
+
section: toNumber(customUnits.section, 1.0),
|
|
188
|
+
major: toNumber(customUnits.major, 1.35),
|
|
189
|
+
},
|
|
190
|
+
minMs: toNumber(custom.minMs, 120),
|
|
191
|
+
maxMs: toNumber(custom.maxMs, 900),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
resolvePauseMsFromUnits(units) {
|
|
195
|
+
const profile = this.getStructuralPauseProfile();
|
|
196
|
+
const rate = Math.max(0.25, Math.min(4, Number(this.ttsConfig.rate || 1)));
|
|
197
|
+
const raw = (profile.baseMs * Math.max(0, units)) / rate;
|
|
198
|
+
return Math.max(profile.minMs, Math.min(profile.maxMs, Math.round(raw)));
|
|
199
|
+
}
|
|
200
|
+
resolveHighlightMode() {
|
|
201
|
+
const providerOptions = (this.ttsConfig.providerOptions || {});
|
|
202
|
+
const configuredMode = providerOptions.highlightMode;
|
|
203
|
+
if (configuredMode === "word" || configuredMode === "sentence") {
|
|
204
|
+
return configuredMode;
|
|
205
|
+
}
|
|
206
|
+
const providerId = (this.currentProvider?.providerId || "").toLowerCase();
|
|
207
|
+
if (providerId === "browser") {
|
|
208
|
+
return "sentence";
|
|
209
|
+
}
|
|
210
|
+
const supportsWordBoundary = this.currentProvider?.getCapabilities().supportsWordBoundary ?? false;
|
|
211
|
+
return supportsWordBoundary ? "word" : "sentence";
|
|
212
|
+
}
|
|
213
|
+
hasExplicitBreakSemantics(text) {
|
|
214
|
+
if (!text.includes("<"))
|
|
215
|
+
return false;
|
|
216
|
+
return /<\s*break\b|<\s*speak\b|<\s*prosody\b|<\s*p\b|<\s*s\b/i.test(text);
|
|
217
|
+
}
|
|
218
|
+
isElementHidden(element) {
|
|
219
|
+
return isElementHiddenForTTS(element);
|
|
220
|
+
}
|
|
221
|
+
extractVisibleText(element, language) {
|
|
222
|
+
return extractVisibleTextFromDOM(element, this.getTextProcessingOptions(language));
|
|
223
|
+
}
|
|
224
|
+
getBoundaryStrength(element) {
|
|
225
|
+
const explicitBreakMs = Number(element.getAttribute("data-tts-break-ms"));
|
|
226
|
+
if (Number.isFinite(explicitBreakMs) && explicitBreakMs > 0)
|
|
227
|
+
return "major";
|
|
228
|
+
const tagName = element.tagName.toUpperCase();
|
|
229
|
+
if (tagName.match(/^H[1-6]$/))
|
|
230
|
+
return "major";
|
|
231
|
+
const role = (element.getAttribute("role") || "").toLowerCase();
|
|
232
|
+
if (role === "heading")
|
|
233
|
+
return "major";
|
|
234
|
+
if (role === "listitem" ||
|
|
235
|
+
role === "row" ||
|
|
236
|
+
role === "option" ||
|
|
237
|
+
role === "radio" ||
|
|
238
|
+
tagName === "LI") {
|
|
239
|
+
return "section";
|
|
240
|
+
}
|
|
241
|
+
if (typeof window !== "undefined") {
|
|
242
|
+
const display = window.getComputedStyle(element).display;
|
|
243
|
+
if (display === "block" ||
|
|
244
|
+
display === "list-item" ||
|
|
245
|
+
display.startsWith("table") ||
|
|
246
|
+
display === "flex" ||
|
|
247
|
+
display === "grid") {
|
|
248
|
+
return "minor";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return "minor";
|
|
252
|
+
}
|
|
253
|
+
getBoundaryAnchor(textNode, root) {
|
|
254
|
+
let current = textNode.parentElement;
|
|
255
|
+
let best = null;
|
|
256
|
+
while (current && current !== root) {
|
|
257
|
+
if (this.isElementHidden(current))
|
|
258
|
+
return null;
|
|
259
|
+
const explicitBreakMs = Number(current.getAttribute("data-tts-break-ms"));
|
|
260
|
+
if (Number.isFinite(explicitBreakMs) && explicitBreakMs > 0) {
|
|
261
|
+
return {
|
|
262
|
+
anchor: current,
|
|
263
|
+
units: explicitBreakMs / this.getStructuralPauseProfile().baseMs,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const role = (current.getAttribute("role") || "").toLowerCase();
|
|
267
|
+
const tagName = current.tagName.toUpperCase();
|
|
268
|
+
if (tagName.match(/^H[1-6]$/) || role === "heading" || role === "listitem" || tagName === "LI") {
|
|
269
|
+
best = current;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
if (!best) {
|
|
273
|
+
if (typeof window !== "undefined") {
|
|
274
|
+
const display = window.getComputedStyle(current).display;
|
|
275
|
+
if (display === "block" ||
|
|
276
|
+
display === "list-item" ||
|
|
277
|
+
display.startsWith("table") ||
|
|
278
|
+
display === "flex" ||
|
|
279
|
+
display === "grid") {
|
|
280
|
+
best = current;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
current = current.parentElement;
|
|
285
|
+
}
|
|
286
|
+
if (!best)
|
|
287
|
+
return null;
|
|
288
|
+
const strength = this.getBoundaryStrength(best);
|
|
289
|
+
return {
|
|
290
|
+
anchor: best,
|
|
291
|
+
units: this.getStructuralPauseProfile().units[strength],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
createSpeechPlan(contentElement, normalizedText) {
|
|
295
|
+
const boundaries = new Map();
|
|
296
|
+
let previousBoundaryAnchor = null;
|
|
297
|
+
const { map } = collectVisibleTextAndMap(contentElement, this.getTextProcessingOptions());
|
|
298
|
+
const nodeStartOffsets = new Map();
|
|
299
|
+
for (const [normalizedIndex, mapping] of map.entries()) {
|
|
300
|
+
if (!nodeStartOffsets.has(mapping.node)) {
|
|
301
|
+
nodeStartOffsets.set(mapping.node, normalizedIndex);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const walker = document.createTreeWalker(contentElement, NodeFilter.SHOW_TEXT);
|
|
305
|
+
let currentNode = walker.nextNode();
|
|
306
|
+
while (currentNode) {
|
|
307
|
+
const textNode = currentNode;
|
|
308
|
+
const parent = textNode.parentElement;
|
|
309
|
+
if (parent && !this.isElementHidden(parent)) {
|
|
310
|
+
const boundary = this.getBoundaryAnchor(textNode, contentElement);
|
|
311
|
+
const boundaryPoint = nodeStartOffsets.get(textNode);
|
|
312
|
+
if (boundary &&
|
|
313
|
+
boundaryPoint !== undefined &&
|
|
314
|
+
boundary.anchor !== previousBoundaryAnchor &&
|
|
315
|
+
boundaryPoint > 0) {
|
|
316
|
+
boundaries.set(boundaryPoint, Math.max(boundaries.get(boundaryPoint) || 0, boundary.units));
|
|
317
|
+
previousBoundaryAnchor = boundary.anchor;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
currentNode = walker.nextNode();
|
|
321
|
+
}
|
|
322
|
+
const points = Array.from(boundaries.keys())
|
|
323
|
+
.filter((point) => point > 0 && point < normalizedText.length)
|
|
324
|
+
.sort((a, b) => a - b);
|
|
325
|
+
const segments = [];
|
|
326
|
+
let start = 0;
|
|
327
|
+
for (const point of points) {
|
|
328
|
+
const raw = normalizedText.substring(start, point);
|
|
329
|
+
const leading = raw.match(/^\s*/)?.[0].length || 0;
|
|
330
|
+
const trailing = raw.match(/\s*$/)?.[0].length || 0;
|
|
331
|
+
const segmentStart = start + leading;
|
|
332
|
+
const segmentEnd = point - trailing;
|
|
333
|
+
if (segmentEnd > segmentStart) {
|
|
334
|
+
const text = normalizedText.substring(segmentStart, segmentEnd);
|
|
335
|
+
segments.push({
|
|
336
|
+
text,
|
|
337
|
+
startOffset: segmentStart,
|
|
338
|
+
pauseMsAfter: this.resolvePauseMsFromUnits(boundaries.get(point) || 0),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
start = point;
|
|
342
|
+
}
|
|
343
|
+
const tail = normalizedText.substring(start);
|
|
344
|
+
const tailLeading = tail.match(/^\s*/)?.[0].length || 0;
|
|
345
|
+
const tailStart = start + tailLeading;
|
|
346
|
+
if (tailStart < normalizedText.length) {
|
|
347
|
+
segments.push({
|
|
348
|
+
text: normalizedText.substring(tailStart).trimEnd(),
|
|
349
|
+
startOffset: tailStart,
|
|
350
|
+
pauseMsAfter: 0,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return segments.filter((segment) => segment.text.trim().length > 0);
|
|
354
|
+
}
|
|
355
|
+
async speakWithPlan(segments, runId, options) {
|
|
356
|
+
if (!this.provider || segments.length === 0)
|
|
357
|
+
return;
|
|
358
|
+
const shouldTrackSentenceProgress = options?.highlightMode === "sentence";
|
|
359
|
+
const providerWithPlan = this.provider;
|
|
360
|
+
if (!shouldTrackSentenceProgress &&
|
|
361
|
+
typeof providerWithPlan.speakSegments === "function") {
|
|
362
|
+
this.currentBoundaryOffset = 0;
|
|
363
|
+
await providerWithPlan.speakSegments(segments);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
for (const segment of segments) {
|
|
367
|
+
if (runId !== this.speakRunId)
|
|
368
|
+
return;
|
|
369
|
+
this.currentBoundaryOffset = segment.startOffset;
|
|
370
|
+
if (shouldTrackSentenceProgress) {
|
|
371
|
+
this.highlightSentenceSegment(segment.startOffset, segment.text);
|
|
372
|
+
}
|
|
373
|
+
await this.provider.speak(segment.text);
|
|
374
|
+
const pauseMs = segment.pauseMsAfter ?? 0;
|
|
375
|
+
if (pauseMs > 0) {
|
|
376
|
+
await new Promise((resolve) => setTimeout(resolve, pauseMs));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
highlightSentenceSegment(startOffset, text) {
|
|
381
|
+
if (!this.highlightCoordinator || typeof document === "undefined")
|
|
382
|
+
return;
|
|
383
|
+
const length = text.trimEnd().length;
|
|
384
|
+
if (length <= 0)
|
|
385
|
+
return;
|
|
386
|
+
const start = this.normalizedToDOM.get(startOffset);
|
|
387
|
+
const end = this.normalizedToDOM.get(startOffset + length - 1);
|
|
388
|
+
if (!start || !end)
|
|
389
|
+
return;
|
|
390
|
+
const range = document.createRange();
|
|
391
|
+
range.setStart(start.node, start.offset);
|
|
392
|
+
range.setEnd(end.node, end.offset + 1);
|
|
393
|
+
this.highlightCoordinator.highlightTTSSentence([range]);
|
|
394
|
+
}
|
|
214
395
|
/**
|
|
215
396
|
* Find text node and offsets for highlighting a word
|
|
216
397
|
*
|
|
@@ -270,11 +451,46 @@ export class TTSService {
|
|
|
270
451
|
if (!this.provider) {
|
|
271
452
|
throw new Error("TTS service not initialized");
|
|
272
453
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
454
|
+
const runId = ++this.speakRunId;
|
|
455
|
+
if (options?.language) {
|
|
456
|
+
const providerOptions = (this.ttsConfig.providerOptions ||
|
|
457
|
+
{});
|
|
458
|
+
const textNormalization = (providerOptions.textNormalization ||
|
|
459
|
+
{});
|
|
460
|
+
const segmenter = (providerOptions.segmenter || {});
|
|
461
|
+
const mergedProviderOptions = {
|
|
462
|
+
...providerOptions,
|
|
463
|
+
locale: options.language,
|
|
464
|
+
textNormalization: {
|
|
465
|
+
...textNormalization,
|
|
466
|
+
locale: options.language,
|
|
467
|
+
},
|
|
468
|
+
segmenter: {
|
|
469
|
+
...segmenter,
|
|
470
|
+
locale: options.language,
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
this.ttsConfig = {
|
|
474
|
+
...this.ttsConfig,
|
|
475
|
+
providerOptions: mergedProviderOptions,
|
|
476
|
+
};
|
|
477
|
+
if ("updateSettings" in this.provider &&
|
|
478
|
+
typeof this.provider.updateSettings ===
|
|
479
|
+
"function") {
|
|
480
|
+
await this.provider.updateSettings({ providerOptions: mergedProviderOptions });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Normalize input text and prefer visible DOM-derived text when available.
|
|
484
|
+
// This keeps generated speech aligned to what learners can actually see.
|
|
485
|
+
const normalizedInputText = normalizeTextForSpeech(text);
|
|
486
|
+
const normalizedText = options?.contentElement
|
|
487
|
+
? this.extractVisibleText(options.contentElement, options.language) ||
|
|
488
|
+
normalizedInputText
|
|
489
|
+
: normalizedInputText;
|
|
276
490
|
// Try to resolve from accessibility catalog if catalogId provided
|
|
277
491
|
let contentToSpeak = normalizedText;
|
|
492
|
+
let usedCatalogSpoken = false;
|
|
493
|
+
let speechSource = "dom-or-input";
|
|
278
494
|
if (options?.catalogId && this.catalogResolver) {
|
|
279
495
|
const catalogContent = this.catalogResolver.getAlternative(options.catalogId, {
|
|
280
496
|
type: "spoken",
|
|
@@ -284,43 +500,78 @@ export class TTSService {
|
|
|
284
500
|
if (catalogContent) {
|
|
285
501
|
// Use pre-authored spoken content from catalog
|
|
286
502
|
contentToSpeak = catalogContent.content;
|
|
503
|
+
usedCatalogSpoken = true;
|
|
504
|
+
speechSource = "catalog-spoken";
|
|
287
505
|
console.debug(`[TTSService] Using catalog content for "${options.catalogId}" (${catalogContent.language})`);
|
|
288
506
|
}
|
|
289
507
|
else {
|
|
290
508
|
console.debug(`[TTSService] No catalog found for "${options.catalogId}", falling back to generated TTS`);
|
|
291
509
|
}
|
|
292
510
|
}
|
|
511
|
+
const preview = contentToSpeak.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
512
|
+
console.debug("[TTSService] Speak resolved content", {
|
|
513
|
+
source: speechSource,
|
|
514
|
+
catalogId: options?.catalogId || null,
|
|
515
|
+
length: contentToSpeak.length,
|
|
516
|
+
preview,
|
|
517
|
+
});
|
|
293
518
|
this.currentText = contentToSpeak;
|
|
294
519
|
this.currentContentElement = options?.contentElement || null;
|
|
295
520
|
this.lastError = null; // Clear previous error
|
|
521
|
+
this.currentBoundaryOffset = 0;
|
|
522
|
+
const highlightMode = options?.highlightModeOverride || this.resolveHighlightMode();
|
|
523
|
+
const shouldUsePlan = !!this.currentContentElement &&
|
|
524
|
+
!usedCatalogSpoken &&
|
|
525
|
+
!this.hasExplicitBreakSemantics(contentToSpeak);
|
|
296
526
|
this.setState(PlaybackState.LOADING);
|
|
297
527
|
// Build position map for highlighting if we have a content element
|
|
298
528
|
if (this.currentContentElement && this.highlightCoordinator) {
|
|
299
|
-
this.buildPositionMap(this.currentContentElement, contentToSpeak);
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
529
|
+
this.buildPositionMap(this.currentContentElement, contentToSpeak, options?.language);
|
|
530
|
+
// Fallback sentence-level highlight for non-planned flows.
|
|
531
|
+
// Planned sentence mode is updated progressively in speakWithPlan().
|
|
532
|
+
if (!(highlightMode === "sentence" && shouldUsePlan)) {
|
|
533
|
+
const range = document.createRange();
|
|
534
|
+
range.selectNodeContents(this.currentContentElement);
|
|
535
|
+
this.highlightCoordinator.highlightTTSSentence([range]);
|
|
536
|
+
console.log("[TTSService] Applied sentence-level highlighting");
|
|
537
|
+
}
|
|
305
538
|
}
|
|
306
539
|
try {
|
|
307
540
|
// Setup word boundary highlighting
|
|
308
|
-
if (
|
|
541
|
+
if (highlightMode === "word" &&
|
|
542
|
+
this.highlightCoordinator &&
|
|
543
|
+
this.currentContentElement) {
|
|
309
544
|
this.provider.onWordBoundary = (word, charIndex, length) => {
|
|
310
545
|
const wordLength = length || word.length;
|
|
311
|
-
const
|
|
546
|
+
const globalIndex = charIndex +
|
|
547
|
+
this.currentBoundaryOffset +
|
|
548
|
+
(options?.wordBoundaryOffset || 0);
|
|
549
|
+
const highlightRange = this.findHighlightRange(globalIndex, wordLength);
|
|
312
550
|
if (highlightRange && this.highlightCoordinator) {
|
|
313
551
|
const highlightText = highlightRange.node.textContent?.substring(highlightRange.start, highlightRange.end) || "";
|
|
314
|
-
console.log(`[TTSService] Highlighting "${highlightText}" (word: "${word}") at position ${
|
|
552
|
+
console.log(`[TTSService] Highlighting "${highlightText}" (word: "${word}") at position ${globalIndex}`);
|
|
315
553
|
this.highlightCoordinator.highlightTTSWord(highlightRange.node, highlightRange.start, highlightRange.end);
|
|
316
554
|
}
|
|
317
555
|
else {
|
|
318
|
-
console.warn(`[TTSService] Could not find highlight range for position ${
|
|
556
|
+
console.warn(`[TTSService] Could not find highlight range for position ${globalIndex}, length ${wordLength}`);
|
|
319
557
|
}
|
|
320
558
|
};
|
|
321
559
|
}
|
|
322
560
|
this.setState(PlaybackState.PLAYING);
|
|
323
|
-
|
|
561
|
+
if (shouldUsePlan && this.currentContentElement) {
|
|
562
|
+
const segments = this.createSpeechPlan(this.currentContentElement, normalizedText);
|
|
563
|
+
if (segments.length > 0) {
|
|
564
|
+
await this.speakWithPlan(segments, runId, { highlightMode });
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
await this.provider.speak(contentToSpeak);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
await this.provider.speak(contentToSpeak);
|
|
572
|
+
}
|
|
573
|
+
if (runId !== this.speakRunId)
|
|
574
|
+
return;
|
|
324
575
|
this.setState(PlaybackState.IDLE);
|
|
325
576
|
// Clear highlights when done
|
|
326
577
|
if (this.highlightCoordinator) {
|
|
@@ -329,10 +580,13 @@ export class TTSService {
|
|
|
329
580
|
// Clear tracking
|
|
330
581
|
this.currentContentElement = null;
|
|
331
582
|
this.normalizedToDOM.clear();
|
|
583
|
+
this.currentBoundaryOffset = 0;
|
|
332
584
|
}
|
|
333
585
|
catch (error) {
|
|
334
586
|
console.error("TTS error:", error);
|
|
335
587
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
588
|
+
if (runId !== this.speakRunId)
|
|
589
|
+
return;
|
|
336
590
|
this.setState(PlaybackState.ERROR);
|
|
337
591
|
// Clear highlights on error
|
|
338
592
|
if (this.highlightCoordinator) {
|
|
@@ -341,6 +595,7 @@ export class TTSService {
|
|
|
341
595
|
// Clear tracking
|
|
342
596
|
this.currentContentElement = null;
|
|
343
597
|
this.normalizedToDOM.clear();
|
|
598
|
+
this.currentBoundaryOffset = 0;
|
|
344
599
|
throw error;
|
|
345
600
|
}
|
|
346
601
|
}
|
|
@@ -352,61 +607,43 @@ export class TTSService {
|
|
|
352
607
|
*
|
|
353
608
|
* @param range DOM Range to speak
|
|
354
609
|
*/
|
|
355
|
-
async speakRange(range) {
|
|
610
|
+
async speakRange(range, options) {
|
|
356
611
|
if (!this.provider) {
|
|
357
612
|
throw new Error("TTS service not initialized");
|
|
358
613
|
}
|
|
359
614
|
const text = range.toString().trim();
|
|
360
615
|
if (!text)
|
|
361
616
|
return;
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
root = document.body;
|
|
617
|
+
// Use explicit content root when provided; otherwise keep highlighting scoped
|
|
618
|
+
// to the selected range's nearest element ancestor.
|
|
619
|
+
const fromOptions = options?.contentRoot || null;
|
|
620
|
+
let root = fromOptions;
|
|
621
|
+
if (!root) {
|
|
622
|
+
const ancestor = range.commonAncestorContainer;
|
|
623
|
+
root =
|
|
624
|
+
ancestor.nodeType === Node.ELEMENT_NODE
|
|
625
|
+
? ancestor
|
|
626
|
+
: ancestor.parentElement;
|
|
373
627
|
}
|
|
628
|
+
if (!root)
|
|
629
|
+
return;
|
|
374
630
|
// Calculate the offset of the range start within the root element
|
|
375
631
|
const beforeRange = document.createRange();
|
|
376
632
|
beforeRange.selectNodeContents(root);
|
|
377
633
|
beforeRange.setEnd(range.startContainer, range.startOffset);
|
|
378
634
|
const textBeforeRange = beforeRange.toString();
|
|
379
|
-
const offset = textBeforeRange
|
|
635
|
+
const offset = normalizeTextForSpeech(textBeforeRange).length;
|
|
380
636
|
console.log("[TTSService] speakRange offset calculation:", {
|
|
381
637
|
selectedText: text,
|
|
382
638
|
textBeforeRange: textBeforeRange.substring(0, 100),
|
|
383
639
|
offset,
|
|
384
640
|
rootTag: root.tagName,
|
|
385
641
|
});
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const adjustedIndex = charIndex + offset;
|
|
392
|
-
// Call the handler from speak() method which will handle the highlighting
|
|
393
|
-
// Pass the adjusted index so highlighting aligns with the actual selection
|
|
394
|
-
if (this.highlightCoordinator && this.currentContentElement) {
|
|
395
|
-
const wordLength = length || word.length;
|
|
396
|
-
const highlightRange = this.findHighlightRange(adjustedIndex, wordLength);
|
|
397
|
-
if (highlightRange) {
|
|
398
|
-
this.highlightCoordinator.highlightTTSWord(highlightRange.node, highlightRange.start, highlightRange.end);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
try {
|
|
403
|
-
// Speak the text with the root element as context
|
|
404
|
-
await this.speak(text, { contentElement: root });
|
|
405
|
-
}
|
|
406
|
-
finally {
|
|
407
|
-
// Restore original handler
|
|
408
|
-
this.provider.onWordBoundary = originalOnWordBoundary;
|
|
409
|
-
}
|
|
642
|
+
// Speak the text with the root element as context
|
|
643
|
+
await this.speak(text, {
|
|
644
|
+
contentElement: root,
|
|
645
|
+
wordBoundaryOffset: offset,
|
|
646
|
+
});
|
|
410
647
|
}
|
|
411
648
|
/**
|
|
412
649
|
* Pause playback
|
|
@@ -436,6 +673,7 @@ export class TTSService {
|
|
|
436
673
|
stop() {
|
|
437
674
|
if (!this.provider)
|
|
438
675
|
return;
|
|
676
|
+
this.speakRunId += 1;
|
|
439
677
|
this.provider.stop();
|
|
440
678
|
this.setState(PlaybackState.IDLE);
|
|
441
679
|
this.currentText = null;
|
|
@@ -446,6 +684,7 @@ export class TTSService {
|
|
|
446
684
|
// Clear tracking
|
|
447
685
|
this.currentContentElement = null;
|
|
448
686
|
this.normalizedToDOM.clear();
|
|
687
|
+
this.currentBoundaryOffset = 0;
|
|
449
688
|
}
|
|
450
689
|
/**
|
|
451
690
|
* Check if currently playing
|