@pie-players/pie-assessment-toolkit 0.3.73 → 0.3.75
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 +469 -318
- package/dist/attempt/AssessmentSession.d.ts +1 -5
- package/dist/attempt/AssessmentSession.js +0 -29
- package/dist/attempt/TestSession.d.ts +1 -20
- package/dist/attempt/TestSession.js +8 -99
- package/dist/components/ItemScope.custom-element.d.ts +1 -0
- package/dist/components/ItemScope.custom-element.js +1 -0
- package/dist/components/ItemToolBar.custom-element.d.ts +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.d.ts +1 -0
- package/dist/components/PieAssessmentToolkit.custom-element.js +13 -13
- package/dist/components/SectionToolBar.custom-element.d.ts +1 -0
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
- package/dist/components/chunks/ItemToolBar-97hx60z4.js +2 -0
- package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +2 -0
- package/dist/components/chunks/ItemToolBar-jx38w99s.js +2 -0
- package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +40 -0
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
- package/dist/components/chunks/sre-engine-jd8cc08x.js +1 -0
- package/dist/components/item-scope-element.d.ts +2 -0
- package/dist/components/item-scope-element.js +2 -0
- package/dist/context/assessment-toolkit-context.d.ts +32 -6
- package/dist/index.d.ts +23 -48
- package/dist/index.js +12 -37
- package/dist/policy/core/PolicySource.d.ts +1 -1
- package/dist/policy/core/ToolPolicyEngine.d.ts +85 -34
- package/dist/policy/core/ToolPolicyEngine.js +148 -46
- package/dist/policy/core/compose-decision.d.ts +28 -7
- package/dist/policy/core/compose-decision.js +75 -54
- package/dist/policy/core/decision-types.d.ts +65 -34
- package/dist/policy/core/feature-decision.d.ts +16 -9
- package/dist/policy/core/feature-decision.js +14 -8
- package/dist/policy/core/pnp-policy-inputs.d.ts +24 -33
- package/dist/policy/core/pnp-policy-inputs.js +28 -56
- package/dist/policy/core/provenance.d.ts +9 -4
- package/dist/policy/core/provenance.js +1 -1
- package/dist/policy/engine.d.ts +7 -23
- package/dist/policy/engine.js +5 -20
- package/dist/policy/sources/PnpPolicySource.d.ts +44 -61
- package/dist/policy/sources/PnpPolicySource.js +117 -118
- package/dist/runtime/SectionControllerBinding.d.ts +279 -0
- package/dist/runtime/SectionControllerBinding.js +450 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
- package/dist/runtime/SectionRuntimeEngine.js +30 -372
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
- package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
- package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
- package/dist/runtime/adapter/dom-event-bridge.js +17 -44
- package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -2
- package/dist/runtime/adapter/subscriber-fanout.js +1 -2
- package/dist/runtime/catalog-context.d.ts +19 -0
- package/dist/runtime/catalog-context.js +28 -0
- package/dist/runtime/composition-emit-scheduler.d.ts +9 -15
- package/dist/runtime/composition-emit-scheduler.js +9 -15
- package/dist/runtime/content-language.d.ts +32 -0
- package/dist/runtime/content-language.js +46 -0
- package/dist/runtime/content-region.d.ts +17 -0
- package/dist/runtime/content-region.js +29 -0
- package/dist/runtime/content-registration.d.ts +13 -0
- package/dist/runtime/content-registration.js +25 -0
- package/dist/runtime/core/SectionEngineCore.d.ts +2 -2
- package/dist/runtime/core/SectionEngineCore.js +2 -2
- package/dist/runtime/core/cohort.d.ts +1 -1
- package/dist/runtime/core/cohort.js +1 -1
- package/dist/runtime/core/engine-input.d.ts +18 -30
- package/dist/runtime/core/engine-input.js +11 -12
- package/dist/runtime/core/engine-output.d.ts +16 -40
- package/dist/runtime/core/engine-output.js +9 -21
- package/dist/runtime/core/engine-readiness.d.ts +5 -37
- package/dist/runtime/core/engine-readiness.js +8 -41
- package/dist/runtime/core/engine-resolver.d.ts +46 -74
- package/dist/runtime/core/engine-resolver.js +18 -43
- package/dist/runtime/core/engine-state.d.ts +28 -36
- package/dist/runtime/core/engine-state.js +18 -21
- package/dist/runtime/core/engine-transition.d.ts +14 -19
- package/dist/runtime/core/engine-transition.js +71 -51
- package/dist/runtime/engine.d.ts +16 -19
- package/dist/runtime/engine.js +12 -19
- package/dist/runtime/page-warnings.d.ts +13 -0
- package/dist/runtime/page-warnings.js +22 -0
- package/dist/runtime/policy-input-forwarding.d.ts +25 -0
- package/dist/runtime/policy-input-forwarding.js +25 -0
- package/dist/runtime/registration-events.d.ts +14 -0
- package/dist/runtime/runtime-event-claim.d.ts +8 -0
- package/dist/runtime/runtime-event-claim.js +15 -0
- package/dist/runtime/shell-event-bridge.d.ts +45 -0
- package/dist/runtime/shell-event-bridge.js +127 -0
- package/dist/runtime/shell-registration.d.ts +73 -0
- package/dist/runtime/shell-registration.js +95 -0
- package/dist/runtime/shell-scope.d.ts +68 -0
- package/dist/runtime/shell-scope.js +188 -0
- package/dist/runtime/tool-host-contract.d.ts +28 -10
- package/dist/runtime/tool-host-contract.js +31 -10
- package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
- package/dist/runtime/unclaimed-registration-watch.js +23 -0
- package/dist/services/ElementToolStateStore.d.ts +14 -10
- package/dist/services/ElementToolStateStore.js +37 -13
- package/dist/services/HighlightCoordinator.d.ts +22 -28
- package/dist/services/HighlightCoordinator.js +431 -327
- package/dist/services/RangeSerializer.d.ts +10 -1
- package/dist/services/RangeSerializer.js +87 -14
- package/dist/services/TTSService.d.ts +130 -69
- package/dist/services/TTSService.js +713 -570
- package/dist/services/ToolCoordinator.d.ts +43 -66
- package/dist/services/ToolCoordinator.js +164 -208
- package/dist/services/ToolRegistry.d.ts +78 -54
- package/dist/services/ToolRegistry.js +107 -105
- package/dist/services/ToolkitCoordinator.d.ts +261 -175
- package/dist/services/ToolkitCoordinator.js +667 -433
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +11 -1
- package/dist/services/catalog-owner.js +25 -0
- package/dist/services/create-tools-config.d.ts +2 -2
- package/dist/services/framework-error-bus.d.ts +3 -6
- package/dist/services/framework-error-bus.js +3 -6
- package/dist/services/framework-error.d.ts +30 -8
- package/dist/services/framework-error.js +9 -7
- package/dist/services/interfaces.d.ts +158 -129
- package/dist/services/section-controller-types.d.ts +15 -0
- package/dist/services/section-session-assignment.d.ts +21 -0
- package/dist/services/section-session-assignment.js +82 -0
- package/dist/services/spoken-audio-cards.d.ts +0 -2
- package/dist/services/spoken-audio-cards.js +1 -3
- package/dist/services/tool-config-defaults.d.ts +3 -3
- package/dist/services/tool-config-defaults.js +3 -3
- package/dist/services/tool-config-validation.d.ts +14 -4
- package/dist/services/tool-config-validation.js +63 -71
- package/dist/services/tool-context.d.ts +0 -8
- package/dist/services/tool-context.js +29 -25
- package/dist/services/tool-icons.d.ts +4 -3
- package/dist/services/tool-icons.js +4 -3
- package/dist/services/tool-instance-id.d.ts +0 -1
- package/dist/services/tool-instance-id.js +0 -6
- package/dist/services/tool-providers/TTSToolProvider.d.ts +41 -80
- package/dist/services/tool-providers/TTSToolProvider.js +81 -40
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +32 -28
- package/dist/services/tool-providers/ToolProviderRegistry.js +121 -107
- package/dist/services/tool-providers/index.d.ts +0 -9
- package/dist/services/tool-providers/index.js +2 -4
- package/dist/services/tool-request.d.ts +27 -12
- package/dist/services/tool-request.js +24 -14
- package/dist/services/toolbar-decided-tools.d.ts +15 -0
- package/dist/services/toolbar-decided-tools.js +28 -0
- package/dist/services/tools-config-normalizer.d.ts +37 -23
- package/dist/services/tools-config-normalizer.js +2 -10
- package/dist/services/tts/browser-provider.d.ts +38 -2
- package/dist/services/tts/browser-provider.js +276 -140
- package/dist/services/tts/catalog-span-alignment.d.ts +2 -17
- package/dist/services/tts/catalog-span-alignment.js +17 -142
- package/dist/services/tts/flat-tree.d.ts +71 -0
- package/dist/services/tts/flat-tree.js +269 -0
- package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
- package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
- package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
- package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
- package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
- package/dist/services/tts/generated-speech/types.d.ts +0 -12
- package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +12 -62
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +38 -5
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +3 -6
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
- package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
- package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
- package/dist/services/tts/math-alignment/index.d.ts +0 -5
- package/dist/services/tts/math-alignment/index.js +5 -25
- package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +9 -1
- package/dist/services/tts/math-alignment/speech-tokenizer.js +38 -36
- package/dist/services/tts/math-alignment/types.d.ts +4 -2
- package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +74 -18
- package/dist/services/tts/math-control-names.d.ts +21 -0
- package/dist/services/tts/math-control-names.js +142 -0
- package/dist/services/tts/math-speech.d.ts +16 -1
- package/dist/services/tts/math-speech.js +55 -19
- package/dist/services/tts/provider-options.d.ts +41 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/sre-engine.d.ts +7 -0
- package/dist/services/tts/sre-engine.js +11 -0
- package/dist/services/tts/sre-locales.d.ts +14 -0
- package/dist/services/tts/sre-locales.js +57 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
- package/dist/services/tts/ssml/spoken-text.js +29 -0
- package/dist/services/tts/start-failure.d.ts +11 -0
- package/dist/services/tts/start-failure.js +19 -0
- package/dist/services/tts/text-processing.d.ts +9 -1
- package/dist/services/tts/text-processing.js +46 -34
- package/dist/services/tts/text-segmentation.d.ts +7 -0
- package/dist/services/tts/text-segmentation.js +47 -0
- package/dist/services/tts-runtime-config.d.ts +44 -18
- package/dist/services/tts-runtime-config.js +22 -93
- package/dist/tools/content-capability-resolution.d.ts +7 -9
- package/dist/tools/content-capability-resolution.js +8 -45
- package/dist/tools/registration.d.ts +58 -0
- package/dist/tools/registration.js +54 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +2 -2
- package/dist/tools/tool-surface-host.d.ts +6 -10
- package/dist/tools/tool-surface-host.js +44 -42
- package/dist/tools/tool-tag-map.d.ts +5 -15
- package/dist/tools/tool-tag-map.js +6 -20
- package/dist/utils/structural-equality.d.ts +11 -0
- package/dist/utils/structural-equality.js +47 -0
- package/package.json +24 -59
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
- package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
- package/dist/components/chunks/ItemToolBar-byq80z42.js +0 -40
- package/dist/components/chunks/ItemToolBar-d36g3aw4.js +0 -49
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -51
- package/dist/item-loader.d.ts +0 -44
- package/dist/item-loader.js +0 -61
- package/dist/policy/core/engine-context.d.ts +0 -29
- package/dist/policy/core/engine-context.js +0 -27
- package/dist/policy/core/policy-source-tag.d.ts +0 -31
- package/dist/policy/core/policy-source-tag.js +0 -28
- package/dist/policy/internal.d.ts +0 -25
- package/dist/policy/internal.js +0 -25
- package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
- package/dist/runtime/adapter/coordinator-bridge.js +0 -126
- package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
- package/dist/runtime/adapter/framework-error-bridge.js +0 -46
- package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
- package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
- package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
- package/dist/runtime/core/engine-stage-derivation.js +0 -29
- package/dist/runtime/engine-context.d.ts +0 -39
- package/dist/runtime/engine-context.js +0 -31
- package/dist/runtime/internal.d.ts +0 -43
- package/dist/runtime/internal.js +0 -45
- package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -80
- package/dist/runtime/section-runtime-engine-host-context.js +0 -95
- package/dist/runtime/stage-emit-gate.d.ts +0 -92
- package/dist/runtime/stage-emit-gate.js +0 -82
- package/dist/services/I18nService.d.ts +0 -47
- package/dist/services/I18nService.js +0 -84
- package/dist/services/ThemeProvider.d.ts +0 -95
- package/dist/services/ThemeProvider.js +0 -334
- package/dist/services/ToolConfigResolver.d.ts +0 -161
- package/dist/services/ToolConfigResolver.js +0 -229
- package/dist/services/catalog-media.d.ts +0 -56
- package/dist/services/catalog-media.js +0 -190
- package/dist/services/createDefaultToolRegistry.d.ts +0 -53
- package/dist/services/createDefaultToolRegistry.js +0 -45
- package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
- package/dist/services/defaultPersonalNeedsProfile.js +0 -26
- package/dist/services/pnp-standard-features.d.ts +0 -238
- package/dist/services/pnp-standard-features.js +0 -338
- package/dist/services/tool-providers/CortexToolProvider.d.ts +0 -13
- package/dist/services/tool-providers/CortexToolProvider.js +0 -32
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +0 -14
- package/dist/services/tool-providers/DesmosToolProvider.js +0 -32
- package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +0 -16
- package/dist/services/tool-providers/GeoGebraToolProvider.js +0 -32
- package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +0 -35
- package/dist/services/tool-providers/LazyCalculatorToolProvider.js +0 -95
- package/dist/services/tts/visual-line-ranges.d.ts +0 -18
- package/dist/services/tts/visual-line-ranges.js +0 -226
- package/dist/tools/client.d.ts +0 -12
- package/dist/tools/client.js +0 -14
- package/dist/tools/index.d.ts +0 -8
- package/dist/tools/index.js +0 -10
- package/dist/tools/internal.d.ts +0 -43
- package/dist/tools/internal.js +0 -41
- package/dist/tools/response-discovery.d.ts +0 -61
- package/dist/tools/response-discovery.js +0 -113
- package/dist/tools/types.d.ts +0 -337
- package/dist/tools/types.js +0 -12
- package/dist/tools/variant-resolver.d.ts +0 -41
- package/dist/tools/variant-resolver.js +0 -207
|
@@ -1,25 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// so multi-word operator phrases ("plus or minus", "divided by") only align for
|
|
7
|
-
// English speech; other locales degrade to coarse region highlighting. A fresh
|
|
8
|
-
// pattern per call because of the global flag's mutable `lastIndex`.
|
|
9
|
-
const TOKEN_PATTERN = createSpeechAlignmentTokenPattern();
|
|
10
|
-
const NUMERIC_WORDS = new Map([
|
|
11
|
-
["zero", "0"],
|
|
12
|
-
["one", "1"],
|
|
13
|
-
["two", "2"],
|
|
14
|
-
["three", "3"],
|
|
15
|
-
["four", "4"],
|
|
16
|
-
["five", "5"],
|
|
17
|
-
["six", "6"],
|
|
18
|
-
["seven", "7"],
|
|
19
|
-
["eight", "8"],
|
|
20
|
-
["nine", "9"],
|
|
21
|
-
["ten", "10"],
|
|
22
|
-
]);
|
|
1
|
+
import { tokenizeAlignmentText, tokenizeSpeechSource, } from "./math-alignment/speech-tokenizer.js";
|
|
2
|
+
import { normalizeTextForSpeech } from "./text-processing.js";
|
|
3
|
+
// LIMITATION (i18n): the phrase table is English, so multi-word operator
|
|
4
|
+
// phrases ("plus or minus", "divided by") only align for English speech; other
|
|
5
|
+
// locales degrade to coarse region highlighting.
|
|
23
6
|
const SPOKEN_PHRASE_TARGETS = [
|
|
24
7
|
{ phrase: ["plus", "or", "minus"], visible: "±", score: 8 },
|
|
25
8
|
{ phrase: ["square", "root"], visible: "√", score: 8 },
|
|
@@ -36,28 +19,12 @@ const SPOKEN_PHRASE_TARGETS = [
|
|
|
36
19
|
{ phrase: ["minus"], visible: "-", score: 5 },
|
|
37
20
|
{ phrase: ["plus"], visible: "+", score: 5 },
|
|
38
21
|
];
|
|
39
|
-
const tokenize = (text) => {
|
|
40
|
-
const tokens = [];
|
|
41
|
-
for (const match of text.matchAll(TOKEN_PATTERN)) {
|
|
42
|
-
const value = match[0];
|
|
43
|
-
const start = match.index ?? 0;
|
|
44
|
-
tokens.push({
|
|
45
|
-
text: value,
|
|
46
|
-
normalized: value.toLowerCase(),
|
|
47
|
-
start,
|
|
48
|
-
end: start + value.length,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return tokens;
|
|
52
|
-
};
|
|
53
|
-
const normalizedTokenValue = (token) => NUMERIC_WORDS.get(token.normalized) || token.normalized;
|
|
54
22
|
const createCandidates = (spokenTokens, visibleTokens) => {
|
|
55
23
|
const candidates = [];
|
|
56
24
|
for (let spokenIndex = 0; spokenIndex < spokenTokens.length; spokenIndex++) {
|
|
57
25
|
for (let visibleIndex = 0; visibleIndex < visibleTokens.length; visibleIndex++) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (spokenValue === visibleValue) {
|
|
26
|
+
if (spokenTokens[spokenIndex].normalized ===
|
|
27
|
+
visibleTokens[visibleIndex].normalized) {
|
|
61
28
|
candidates.push({
|
|
62
29
|
spokenStartToken: spokenIndex,
|
|
63
30
|
spokenEndToken: spokenIndex + 1,
|
|
@@ -74,7 +41,9 @@ const createCandidates = (spokenTokens, visibleTokens) => {
|
|
|
74
41
|
continue;
|
|
75
42
|
}
|
|
76
43
|
for (let visibleIndex = 0; visibleIndex < visibleTokens.length; visibleIndex++) {
|
|
77
|
-
|
|
44
|
+
// Glyph targets compare against the visible text as written, so a
|
|
45
|
+
// visible number word ("two") never stands in for the digit "2".
|
|
46
|
+
if (visibleTokens[visibleIndex].text !== phraseTarget.visible) {
|
|
78
47
|
continue;
|
|
79
48
|
}
|
|
80
49
|
candidates.push({
|
|
@@ -151,21 +120,20 @@ const scoreConfidence = (anchors, spokenTokens, visibleTokens) => {
|
|
|
151
120
|
return Math.min(1, tokenCoverage * averageScore);
|
|
152
121
|
};
|
|
153
122
|
export const createCatalogSpanAlignment = (args) => {
|
|
154
|
-
const
|
|
123
|
+
const speech = tokenizeSpeechSource({ speechText: args.speechText });
|
|
155
124
|
const visibleText = normalizeTextForSpeech(args.visibleText);
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const anchors = extracted.unsupportedSemantic
|
|
125
|
+
const spokenTokens = speech.tokens;
|
|
126
|
+
const visibleTokens = tokenizeAlignmentText(visibleText);
|
|
127
|
+
const anchors = speech.unsupportedSemantic
|
|
160
128
|
? []
|
|
161
129
|
: computeAnchors(spokenTokens, visibleTokens);
|
|
162
130
|
const confidence = scoreConfidence(anchors, spokenTokens, visibleTokens);
|
|
163
|
-
const isExact = spokenText === visibleText && spokenText.length > 0;
|
|
131
|
+
const isExact = speech.spokenText === visibleText && speech.spokenText.length > 0;
|
|
164
132
|
const hasShortSingleExactAnchor = anchors.length === 1 &&
|
|
165
133
|
anchors[0].score === 10 &&
|
|
166
134
|
spokenTokens.length <= 2 &&
|
|
167
135
|
visibleTokens.length <= 2;
|
|
168
|
-
const playbackMode =
|
|
136
|
+
const playbackMode = speech.unsupportedSemantic
|
|
169
137
|
? "region-fallback"
|
|
170
138
|
: isExact
|
|
171
139
|
? "exact-word"
|
|
@@ -174,107 +142,14 @@ export const createCatalogSpanAlignment = (args) => {
|
|
|
174
142
|
? "anchor-span"
|
|
175
143
|
: "region-fallback"
|
|
176
144
|
: "region-fallback";
|
|
177
|
-
const boundaryOffsetMode = extracted.unsupportedSemantic
|
|
178
|
-
? "unsupported"
|
|
179
|
-
: extracted.hasMarkup
|
|
180
|
-
? "raw-ssml"
|
|
181
|
-
: "plain-spoken-text";
|
|
182
145
|
return {
|
|
183
|
-
|
|
184
|
-
spokenText,
|
|
146
|
+
speech,
|
|
185
147
|
visibleText,
|
|
186
148
|
playbackMode,
|
|
187
|
-
boundaryOffsetMode,
|
|
188
149
|
anchors,
|
|
189
150
|
confidence,
|
|
190
|
-
spokenTokens,
|
|
191
|
-
visibleTokens,
|
|
192
|
-
rawToSpokenOffsetMap: extracted.rawToSpokenOffsetMap,
|
|
193
|
-
};
|
|
194
|
-
};
|
|
195
|
-
const findTokenAtOffset = (tokens, offset) => tokens.find((token) => token.start <= offset && offset < token.end) || null;
|
|
196
|
-
const normalizeBoundaryWord = (word) => {
|
|
197
|
-
if (!word)
|
|
198
|
-
return null;
|
|
199
|
-
if (/^<[^>]+>$/.test(word.trim()))
|
|
200
|
-
return null;
|
|
201
|
-
const token = tokenize(word)[0];
|
|
202
|
-
if (!token)
|
|
203
|
-
return null;
|
|
204
|
-
return NUMERIC_WORDS.get(token.normalized) || token.normalized;
|
|
205
|
-
};
|
|
206
|
-
const candidateMatchesBoundaryWord = (alignment, candidate, boundaryWord) => {
|
|
207
|
-
const normalizedBoundary = normalizeBoundaryWord(boundaryWord);
|
|
208
|
-
if (!candidate || !normalizedBoundary)
|
|
209
|
-
return false;
|
|
210
|
-
const token = findTokenAtOffset(alignment.spokenTokens, candidate.start);
|
|
211
|
-
if (!token)
|
|
212
|
-
return false;
|
|
213
|
-
return normalizedTokenValue(token) === normalizedBoundary;
|
|
214
|
-
};
|
|
215
|
-
const mapRawOffsetToSpokenOffset = (alignment, position) => {
|
|
216
|
-
const direct = alignment.rawToSpokenOffsetMap.get(position);
|
|
217
|
-
if (direct !== undefined)
|
|
218
|
-
return direct;
|
|
219
|
-
for (let offset = position; offset < alignment.speechText.length; offset++) {
|
|
220
|
-
const mapped = alignment.rawToSpokenOffsetMap.get(offset);
|
|
221
|
-
if (mapped !== undefined)
|
|
222
|
-
return mapped;
|
|
223
|
-
if (alignment.speechText[offset] === ">")
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
return null;
|
|
227
|
-
};
|
|
228
|
-
const resolveRawBoundaryOffset = (alignment, position, safeLength) => {
|
|
229
|
-
const mappedStart = mapRawOffsetToSpokenOffset(alignment, position);
|
|
230
|
-
if (mappedStart === null)
|
|
231
|
-
return null;
|
|
232
|
-
const mappedEnd = mapRawOffsetToSpokenOffset(alignment, Math.max(position, position + safeLength - 1));
|
|
233
|
-
const token = findTokenAtOffset(alignment.spokenTokens, mappedStart);
|
|
234
|
-
const mappedLength = mappedEnd !== null && mappedEnd >= mappedStart
|
|
235
|
-
? mappedEnd - mappedStart + 1
|
|
236
|
-
: token
|
|
237
|
-
? token.end - mappedStart
|
|
238
|
-
: safeLength;
|
|
239
|
-
return { start: mappedStart, length: Math.max(1, mappedLength) };
|
|
240
|
-
};
|
|
241
|
-
const resolvePlainBoundaryOffset = (alignment, position, safeLength) => {
|
|
242
|
-
if (position < 0 || position >= alignment.spokenText.length)
|
|
243
|
-
return null;
|
|
244
|
-
const token = findTokenAtOffset(alignment.spokenTokens, position);
|
|
245
|
-
if (!token)
|
|
246
|
-
return null;
|
|
247
|
-
return {
|
|
248
|
-
start: position,
|
|
249
|
-
length: Math.min(safeLength, token.end - position),
|
|
250
151
|
};
|
|
251
152
|
};
|
|
252
|
-
export const resolveSpokenBoundaryOffset = (alignment, position, length = 1, boundaryWord) => {
|
|
253
|
-
if (alignment.boundaryOffsetMode === "unsupported" ||
|
|
254
|
-
!Number.isFinite(position)) {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
const safeLength = Math.max(1, Number.isFinite(length) ? length : 1);
|
|
258
|
-
if (boundaryWord && /^<[^>]+>$/.test(boundaryWord.trim()))
|
|
259
|
-
return null;
|
|
260
|
-
if (alignment.boundaryOffsetMode === "raw-ssml") {
|
|
261
|
-
const rawCandidate = resolveRawBoundaryOffset(alignment, position, safeLength);
|
|
262
|
-
const plainCandidate = resolvePlainBoundaryOffset(alignment, position, safeLength);
|
|
263
|
-
if (boundaryWord) {
|
|
264
|
-
if (candidateMatchesBoundaryWord(alignment, rawCandidate, boundaryWord)) {
|
|
265
|
-
return rawCandidate;
|
|
266
|
-
}
|
|
267
|
-
if (candidateMatchesBoundaryWord(alignment, plainCandidate, boundaryWord)) {
|
|
268
|
-
return plainCandidate;
|
|
269
|
-
}
|
|
270
|
-
if (normalizeBoundaryWord(boundaryWord))
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
return rawCandidate || plainCandidate;
|
|
274
|
-
}
|
|
275
|
-
return (resolvePlainBoundaryOffset(alignment, position, safeLength) ||
|
|
276
|
-
resolveRawBoundaryOffset(alignment, position, safeLength));
|
|
277
|
-
};
|
|
278
153
|
export const resolveVisibleSpanForBoundary = (alignment, spokenOffset) => {
|
|
279
154
|
if (alignment.playbackMode === "region-fallback")
|
|
280
155
|
return null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tree read-aloud walks: the flat tree, which is the tree the page renders.
|
|
3
|
+
*
|
|
4
|
+
* An element with an open shadow root contributes that root's children in place
|
|
5
|
+
* of its own light children, and a `<slot>` inside a shadow root contributes the
|
|
6
|
+
* nodes assigned to it, flattened through nested slots, so a host's light
|
|
7
|
+
* children are reached once, at the slot that renders them. Closed shadow roots
|
|
8
|
+
* are invisible to script, so their hosts are walked as light DOM.
|
|
9
|
+
*
|
|
10
|
+
* Toolkit chrome is skipped wherever it appears: toolbars and tool elements
|
|
11
|
+
* render their controls into open shadow roots, and descending into them would
|
|
12
|
+
* read button labels as content. So are the empty `aria-hidden` anchors the
|
|
13
|
+
* shells and the toolkit keep beside their slots, and elements whose text never
|
|
14
|
+
* renders (`style`, `script`, `noscript`, `template`).
|
|
15
|
+
*
|
|
16
|
+
* Node types are compared as the spec's numbers, so this module reads no DOM
|
|
17
|
+
* global, at import or at call time.
|
|
18
|
+
*/
|
|
19
|
+
export declare const isShadowRootNode: (node: Node | null | undefined) => node is ShadowRoot;
|
|
20
|
+
/** Toolbars, tool elements and debug panels: chrome around content, never content. */
|
|
21
|
+
export declare const isToolkitChromeElement: (element: Element) => boolean;
|
|
22
|
+
/** `node`'s children in the flat tree, with skipped elements left out. */
|
|
23
|
+
export declare const flatTreeChildNodes: (node: Node) => Node[];
|
|
24
|
+
/**
|
|
25
|
+
* What a visitor returns: `"skip"` leaves the node's subtree out, `"stop"` ends
|
|
26
|
+
* the walk, anything else descends.
|
|
27
|
+
*/
|
|
28
|
+
export type FlatTreeVisitResult = "skip" | "stop" | undefined;
|
|
29
|
+
/** Visits `root`'s flat-tree descendants in rendering order; `root` itself is not visited. */
|
|
30
|
+
export declare const walkFlatTree: (root: Node, visit: (node: Node) => FlatTreeVisitResult) => void;
|
|
31
|
+
/** `root`'s text nodes in rendering order. */
|
|
32
|
+
export declare const flatTreeTextNodes: (root: Node) => Text[];
|
|
33
|
+
/** `textContent` over the flat tree: shadow content in, skipped elements out. */
|
|
34
|
+
export declare const flatTextContent: (root: Node) => string;
|
|
35
|
+
/** Elements under `root` matching `selector`, in rendering order. */
|
|
36
|
+
export declare const flatQuerySelectorAll: (root: Node, selector: string) => Element[];
|
|
37
|
+
export declare const flatQuerySelector: (root: Node, selector: string) => Element | null;
|
|
38
|
+
/**
|
|
39
|
+
* `node`'s parent in the flat tree: the slot rendering it, else its parent, with
|
|
40
|
+
* a shadow root standing for its host.
|
|
41
|
+
*/
|
|
42
|
+
export declare const flatTreeParentNode: (node: Node) => Node | null;
|
|
43
|
+
export declare const flatTreeParentElement: (node: Node) => Element | null;
|
|
44
|
+
/** The nearest element at or above `node` in the flat tree that matches `selector`. */
|
|
45
|
+
export declare const flatTreeClosest: (node: Node, selector: string) => Element | null;
|
|
46
|
+
/**
|
|
47
|
+
* `node`'s parent element, continuing from a shadow root to its host. This is the
|
|
48
|
+
* chain inheritance follows (`lang`, regions an author marks up), and for slotted
|
|
49
|
+
* content it stays in the light tree the author wrote.
|
|
50
|
+
*/
|
|
51
|
+
export declare const composedParentElement: (node: Node) => Element | null;
|
|
52
|
+
/**
|
|
53
|
+
* The nearest element at or above `node` on the composed chain that matches
|
|
54
|
+
* `selector`, or null. With `boundary`, the climb ends after testing it.
|
|
55
|
+
*/
|
|
56
|
+
export declare const composedClosest: (node: Node, selector: string, boundary?: Element | null) => Element | null;
|
|
57
|
+
/** `contains` across shadow boundaries: whether `node` is `container` or below it. */
|
|
58
|
+
export declare const composedContains: (container: Node | null | undefined, node: Node | null | undefined) => boolean;
|
|
59
|
+
/**
|
|
60
|
+
* `node`, or the shadow host above it, that lies in the tree rooted at
|
|
61
|
+
* `treeRoot`; null when `node` is not inside that tree at any depth.
|
|
62
|
+
*/
|
|
63
|
+
export declare const retargetToTree: (node: Node, treeRoot: Node) => Node | null;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the character at `offset` in `node` lies inside `range`. A live range
|
|
66
|
+
* holds nodes of one tree only, so a node in a shadow tree below it counts as
|
|
67
|
+
* inside when the range holds that tree's host whole.
|
|
68
|
+
*/
|
|
69
|
+
export declare const rangeHoldsTextPosition: (range: Range, node: Text, offset: number) => boolean;
|
|
70
|
+
/** Whether `range` intersects `node`, counting a shadow tree below the range as {@link rangeHoldsTextPosition} does. */
|
|
71
|
+
export declare const rangeIntersectsComposedNode: (range: Range, node: Node) => boolean;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tree read-aloud walks: the flat tree, which is the tree the page renders.
|
|
3
|
+
*
|
|
4
|
+
* An element with an open shadow root contributes that root's children in place
|
|
5
|
+
* of its own light children, and a `<slot>` inside a shadow root contributes the
|
|
6
|
+
* nodes assigned to it, flattened through nested slots, so a host's light
|
|
7
|
+
* children are reached once, at the slot that renders them. Closed shadow roots
|
|
8
|
+
* are invisible to script, so their hosts are walked as light DOM.
|
|
9
|
+
*
|
|
10
|
+
* Toolkit chrome is skipped wherever it appears: toolbars and tool elements
|
|
11
|
+
* render their controls into open shadow roots, and descending into them would
|
|
12
|
+
* read button labels as content. So are the empty `aria-hidden` anchors the
|
|
13
|
+
* shells and the toolkit keep beside their slots, and elements whose text never
|
|
14
|
+
* renders (`style`, `script`, `noscript`, `template`).
|
|
15
|
+
*
|
|
16
|
+
* Node types are compared as the spec's numbers, so this module reads no DOM
|
|
17
|
+
* global, at import or at call time.
|
|
18
|
+
*/
|
|
19
|
+
const ELEMENT_NODE = 1;
|
|
20
|
+
const TEXT_NODE = 3;
|
|
21
|
+
const DOCUMENT_FRAGMENT_NODE = 11;
|
|
22
|
+
const CHROME_TAGS = new Set([
|
|
23
|
+
"pie-item-toolbar",
|
|
24
|
+
"pie-section-toolbar",
|
|
25
|
+
"pie-item-player-session-debugger",
|
|
26
|
+
]);
|
|
27
|
+
const CHROME_TAG_PREFIXES = ["pie-tool-", "pie-section-player-tools-"];
|
|
28
|
+
const UNRENDERED_TEXT_TAGS = new Set([
|
|
29
|
+
"style",
|
|
30
|
+
"script",
|
|
31
|
+
"noscript",
|
|
32
|
+
"template",
|
|
33
|
+
]);
|
|
34
|
+
export const isShadowRootNode = (node) => !!node &&
|
|
35
|
+
node.nodeType === DOCUMENT_FRAGMENT_NODE &&
|
|
36
|
+
node.host?.nodeType === ELEMENT_NODE;
|
|
37
|
+
/** Toolbars, tool elements and debug panels: chrome around content, never content. */
|
|
38
|
+
export const isToolkitChromeElement = (element) => {
|
|
39
|
+
const tag = element.localName?.toLowerCase() || "";
|
|
40
|
+
if (CHROME_TAGS.has(tag))
|
|
41
|
+
return true;
|
|
42
|
+
return CHROME_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix));
|
|
43
|
+
};
|
|
44
|
+
const isSkippedElement = (element) => {
|
|
45
|
+
const tag = element.localName?.toLowerCase() || "";
|
|
46
|
+
if (UNRENDERED_TEXT_TAGS.has(tag))
|
|
47
|
+
return true;
|
|
48
|
+
if (isToolkitChromeElement(element))
|
|
49
|
+
return true;
|
|
50
|
+
// A shell's anchor: an `aria-hidden` element at the top of a shadow root,
|
|
51
|
+
// there to reach the host's context and holding nothing to read.
|
|
52
|
+
return (isShadowRootNode(element.parentNode) &&
|
|
53
|
+
element.getAttribute("aria-hidden") === "true");
|
|
54
|
+
};
|
|
55
|
+
const slotFlattenedNodes = (slot) => {
|
|
56
|
+
if (!isShadowRootNode(slot.getRootNode?.()))
|
|
57
|
+
return null;
|
|
58
|
+
const assignedNodes = slot.assignedNodes;
|
|
59
|
+
if (typeof assignedNodes !== "function")
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
return assignedNodes.call(slot, { flatten: true });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/** `node`'s children in the flat tree, with skipped elements left out. */
|
|
69
|
+
export const flatTreeChildNodes = (node) => {
|
|
70
|
+
let children = node.childNodes;
|
|
71
|
+
if (node.nodeType === ELEMENT_NODE) {
|
|
72
|
+
const element = node;
|
|
73
|
+
const shadowRoot = element.shadowRoot;
|
|
74
|
+
if (shadowRoot) {
|
|
75
|
+
children = shadowRoot.childNodes;
|
|
76
|
+
}
|
|
77
|
+
else if (element.localName?.toLowerCase() === "slot") {
|
|
78
|
+
children = slotFlattenedNodes(element) ?? element.childNodes;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const result = [];
|
|
82
|
+
for (const child of Array.from(children || [])) {
|
|
83
|
+
if (child.nodeType === ELEMENT_NODE && isSkippedElement(child)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
result.push(child);
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
};
|
|
90
|
+
/** Visits `root`'s flat-tree descendants in rendering order; `root` itself is not visited. */
|
|
91
|
+
export const walkFlatTree = (root, visit) => {
|
|
92
|
+
const descend = (node) => {
|
|
93
|
+
for (const child of flatTreeChildNodes(node)) {
|
|
94
|
+
const result = visit(child);
|
|
95
|
+
if (result === "stop")
|
|
96
|
+
return false;
|
|
97
|
+
if (result === "skip")
|
|
98
|
+
continue;
|
|
99
|
+
if (!descend(child))
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
};
|
|
104
|
+
descend(root);
|
|
105
|
+
};
|
|
106
|
+
/** `root`'s text nodes in rendering order. */
|
|
107
|
+
export const flatTreeTextNodes = (root) => {
|
|
108
|
+
const nodes = [];
|
|
109
|
+
walkFlatTree(root, (node) => {
|
|
110
|
+
if (node.nodeType === TEXT_NODE)
|
|
111
|
+
nodes.push(node);
|
|
112
|
+
return undefined;
|
|
113
|
+
});
|
|
114
|
+
return nodes;
|
|
115
|
+
};
|
|
116
|
+
/** `textContent` over the flat tree: shadow content in, skipped elements out. */
|
|
117
|
+
export const flatTextContent = (root) => flatTreeTextNodes(root)
|
|
118
|
+
.map((node) => node.data)
|
|
119
|
+
.join("");
|
|
120
|
+
/** Elements under `root` matching `selector`, in rendering order. */
|
|
121
|
+
export const flatQuerySelectorAll = (root, selector) => {
|
|
122
|
+
const matches = [];
|
|
123
|
+
walkFlatTree(root, (node) => {
|
|
124
|
+
if (node.nodeType === ELEMENT_NODE && node.matches(selector)) {
|
|
125
|
+
matches.push(node);
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
});
|
|
129
|
+
return matches;
|
|
130
|
+
};
|
|
131
|
+
export const flatQuerySelector = (root, selector) => {
|
|
132
|
+
let match = null;
|
|
133
|
+
walkFlatTree(root, (node) => {
|
|
134
|
+
if (node.nodeType === ELEMENT_NODE && node.matches(selector)) {
|
|
135
|
+
match = node;
|
|
136
|
+
return "stop";
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
});
|
|
140
|
+
return match;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* `node`'s parent in the flat tree: the slot rendering it, else its parent, with
|
|
144
|
+
* a shadow root standing for its host.
|
|
145
|
+
*/
|
|
146
|
+
export const flatTreeParentNode = (node) => {
|
|
147
|
+
const slot = node.assignedSlot;
|
|
148
|
+
if (slot)
|
|
149
|
+
return slot;
|
|
150
|
+
const parent = node.parentNode;
|
|
151
|
+
if (isShadowRootNode(parent))
|
|
152
|
+
return parent.host;
|
|
153
|
+
return parent;
|
|
154
|
+
};
|
|
155
|
+
export const flatTreeParentElement = (node) => {
|
|
156
|
+
let parent = flatTreeParentNode(node);
|
|
157
|
+
while (parent && parent.nodeType !== ELEMENT_NODE) {
|
|
158
|
+
parent = flatTreeParentNode(parent);
|
|
159
|
+
}
|
|
160
|
+
return parent;
|
|
161
|
+
};
|
|
162
|
+
/** The nearest element at or above `node` in the flat tree that matches `selector`. */
|
|
163
|
+
export const flatTreeClosest = (node, selector) => {
|
|
164
|
+
let current = node.nodeType === ELEMENT_NODE
|
|
165
|
+
? node
|
|
166
|
+
: flatTreeParentElement(node);
|
|
167
|
+
while (current) {
|
|
168
|
+
if (current.matches(selector))
|
|
169
|
+
return current;
|
|
170
|
+
current = flatTreeParentElement(current);
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* `node`'s parent element, continuing from a shadow root to its host. This is the
|
|
176
|
+
* chain inheritance follows (`lang`, regions an author marks up), and for slotted
|
|
177
|
+
* content it stays in the light tree the author wrote.
|
|
178
|
+
*/
|
|
179
|
+
export const composedParentElement = (node) => {
|
|
180
|
+
if (node.parentElement)
|
|
181
|
+
return node.parentElement;
|
|
182
|
+
const parent = node.parentNode;
|
|
183
|
+
return isShadowRootNode(parent) ? parent.host : null;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* The nearest element at or above `node` on the composed chain that matches
|
|
187
|
+
* `selector`, or null. With `boundary`, the climb ends after testing it.
|
|
188
|
+
*/
|
|
189
|
+
export const composedClosest = (node, selector, boundary) => {
|
|
190
|
+
let current = node.nodeType === ELEMENT_NODE
|
|
191
|
+
? node
|
|
192
|
+
: composedParentElement(node);
|
|
193
|
+
while (current) {
|
|
194
|
+
if (current.matches(selector))
|
|
195
|
+
return current;
|
|
196
|
+
if (boundary && current === boundary)
|
|
197
|
+
return null;
|
|
198
|
+
current = composedParentElement(current);
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
};
|
|
202
|
+
/** `contains` across shadow boundaries: whether `node` is `container` or below it. */
|
|
203
|
+
export const composedContains = (container, node) => {
|
|
204
|
+
if (!container || !node)
|
|
205
|
+
return false;
|
|
206
|
+
let current = node;
|
|
207
|
+
while (current) {
|
|
208
|
+
if (current === container || container.contains(current))
|
|
209
|
+
return true;
|
|
210
|
+
const root = current.getRootNode?.();
|
|
211
|
+
current = isShadowRootNode(root) ? root.host : null;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* `node`, or the shadow host above it, that lies in the tree rooted at
|
|
217
|
+
* `treeRoot`; null when `node` is not inside that tree at any depth.
|
|
218
|
+
*/
|
|
219
|
+
export const retargetToTree = (node, treeRoot) => {
|
|
220
|
+
let current = node;
|
|
221
|
+
while (current) {
|
|
222
|
+
const root = current.getRootNode?.();
|
|
223
|
+
if (root === treeRoot)
|
|
224
|
+
return current;
|
|
225
|
+
current = isShadowRootNode(root) ? root.host : null;
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Whether the character at `offset` in `node` lies inside `range`. A live range
|
|
231
|
+
* holds nodes of one tree only, so a node in a shadow tree below it counts as
|
|
232
|
+
* inside when the range holds that tree's host whole.
|
|
233
|
+
*/
|
|
234
|
+
export const rangeHoldsTextPosition = (range, node, offset) => {
|
|
235
|
+
try {
|
|
236
|
+
const treeRoot = range.startContainer.getRootNode?.();
|
|
237
|
+
if (!treeRoot || node.getRootNode?.() === treeRoot) {
|
|
238
|
+
return (range.comparePoint(node, offset) === 0 &&
|
|
239
|
+
range.comparePoint(node, Math.min(offset + 1, node.length)) === 0);
|
|
240
|
+
}
|
|
241
|
+
return rangeHoldsNodeWhole(range, retargetToTree(node, treeRoot));
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
/** Whether `range` intersects `node`, counting a shadow tree below the range as {@link rangeHoldsTextPosition} does. */
|
|
248
|
+
export const rangeIntersectsComposedNode = (range, node) => {
|
|
249
|
+
try {
|
|
250
|
+
const treeRoot = range.startContainer.getRootNode?.();
|
|
251
|
+
if (!treeRoot || node.getRootNode?.() === treeRoot) {
|
|
252
|
+
return range.intersectsNode(node);
|
|
253
|
+
}
|
|
254
|
+
return rangeHoldsNodeWhole(range, retargetToTree(node, treeRoot));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const rangeHoldsNodeWhole = (range, node) => {
|
|
261
|
+
const parent = node?.parentNode;
|
|
262
|
+
if (!node || !parent)
|
|
263
|
+
return false;
|
|
264
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, node);
|
|
265
|
+
if (index < 0)
|
|
266
|
+
return false;
|
|
267
|
+
return (range.comparePoint(parent, index) === 0 &&
|
|
268
|
+
range.comparePoint(parent, index + 1) === 0);
|
|
269
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MathAwareSpeechChunk } from "../math-aware-text-processing.js";
|
|
2
|
-
import { type ResolvedMathSpeech, type SREMathSpeechOptions } from "../math-speech.js";
|
|
2
|
+
import { type ResolveMathSpeechOptions, type ResolvedMathSpeech, type SREMathSpeechOptions } from "../math-speech.js";
|
|
3
3
|
import type { AssembledSpeech } from "./types.js";
|
|
4
4
|
type MathChunk = Extract<MathAwareSpeechChunk, {
|
|
5
5
|
type: "math";
|
|
@@ -13,6 +13,8 @@ export type MathSpeechResolver = (chunk: MathChunk, options: {
|
|
|
13
13
|
language?: string;
|
|
14
14
|
produceSsml?: boolean;
|
|
15
15
|
mathSpeech?: SREMathSpeechOptions;
|
|
16
|
+
/** The read's one engine load, shared by its equations. */
|
|
17
|
+
loadSre?: ResolveMathSpeechOptions["loadSre"];
|
|
16
18
|
}) => Promise<ResolvedMathSpeech>;
|
|
17
19
|
/**
|
|
18
20
|
* Assemble an anchor-free speech plan from already-collected math-aware chunks.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveMathSpeechFromChunks, } from "../math-speech.js";
|
|
1
|
+
import { createReadSreLoad, resolveMathSpeechFromChunks, } from "../math-speech.js";
|
|
2
2
|
import { normalizeTextForSpeech } from "../text-processing.js";
|
|
3
3
|
import { segmentSentences as segmentTextToSentences } from "../text-segmentation.js";
|
|
4
4
|
const defaultMathSpeechResolver = (chunk, options) => resolveMathSpeechFromChunks([chunk], options);
|
|
@@ -37,6 +37,7 @@ const shouldPreserveProseChunk = (chunk) => {
|
|
|
37
37
|
*/
|
|
38
38
|
export const assembleGeneratedSpeech = async (args) => {
|
|
39
39
|
const resolveMathSpeech = args.resolveMathSpeech ?? defaultMathSpeechResolver;
|
|
40
|
+
const loadSre = createReadSreLoad(args.mathSpeech);
|
|
40
41
|
const { visibleText } = args;
|
|
41
42
|
const segments = [];
|
|
42
43
|
// Cursor-based span tracking:
|
|
@@ -84,6 +85,7 @@ export const assembleGeneratedSpeech = async (args) => {
|
|
|
84
85
|
language: args.language,
|
|
85
86
|
produceSsml: args.produceSsml,
|
|
86
87
|
mathSpeech: args.mathSpeech,
|
|
88
|
+
loadSre,
|
|
87
89
|
});
|
|
88
90
|
const usedFallback = !generated.speechText;
|
|
89
91
|
const spokenText = generated.speechText || chunk.fallbackText;
|
|
@@ -16,6 +16,8 @@ export interface BuildGeneratedSpeechResult {
|
|
|
16
16
|
*/
|
|
17
17
|
export declare const buildGeneratedSpeechFromRoot: (args: {
|
|
18
18
|
contentRoot: Element;
|
|
19
|
+
/** Limit the plan to the part of `contentRoot` this range selects. */
|
|
20
|
+
range?: Range;
|
|
19
21
|
language?: string;
|
|
20
22
|
mathSpeech?: SREMathSpeechOptions;
|
|
21
23
|
textProcessingOptions?: TextProcessingOptions;
|
|
@@ -17,7 +17,7 @@ const sliceVisibleMap = (map, start, end) => {
|
|
|
17
17
|
* entry point that touches the DOM; assembly and serialization stay pure.
|
|
18
18
|
*/
|
|
19
19
|
export const buildGeneratedSpeechFromRoot = async (args) => {
|
|
20
|
-
const extracted = collectMathAwareTextAndMap(args.contentRoot, args.textProcessingOptions);
|
|
20
|
+
const extracted = collectMathAwareTextAndMap(args.contentRoot, args.textProcessingOptions, args.range);
|
|
21
21
|
const assembled = await assembleGeneratedSpeech({
|
|
22
22
|
chunks: extracted.chunks,
|
|
23
23
|
visibleText: extracted.visibleText,
|
|
@@ -7,7 +7,9 @@ export interface MathSpeechCacheOptions {
|
|
|
7
7
|
/**
|
|
8
8
|
* Build a memoized {@link MathSpeechResolver}.
|
|
9
9
|
*
|
|
10
|
-
* Caches only DOM-free spoken-text results
|
|
10
|
+
* Caches only DOM-free spoken-text results that SRE produced, so an equation
|
|
11
|
+
* read as its visible text after a failed engine load is resolved again on
|
|
12
|
+
* the next read. Keyed by
|
|
11
13
|
* `(SRE version, locale, output mode, SRE mathSpeech options, MathML source)`.
|
|
12
14
|
* Never caches DOM nodes or maps. Bounded by a simple LRU so long sessions do
|
|
13
15
|
* not grow unbounded.
|
|
@@ -35,7 +35,9 @@ const cacheKeyFor = (language, produceSsml, mathSpeech, mathml) => `${SRE_CACHE_
|
|
|
35
35
|
/**
|
|
36
36
|
* Build a memoized {@link MathSpeechResolver}.
|
|
37
37
|
*
|
|
38
|
-
* Caches only DOM-free spoken-text results
|
|
38
|
+
* Caches only DOM-free spoken-text results that SRE produced, so an equation
|
|
39
|
+
* read as its visible text after a failed engine load is resolved again on
|
|
40
|
+
* the next read. Keyed by
|
|
39
41
|
* `(SRE version, locale, output mode, SRE mathSpeech options, MathML source)`.
|
|
40
42
|
* Never caches DOM nodes or maps. Bounded by a simple LRU so long sessions do
|
|
41
43
|
* not grow unbounded.
|
|
@@ -66,6 +68,8 @@ export const createMemoizedMathSpeechResolver = (options = {}) => {
|
|
|
66
68
|
return cached;
|
|
67
69
|
}
|
|
68
70
|
const resolved = await resolve(chunk, { ...opts, mathSpeech });
|
|
71
|
+
if (resolved.usedFallback)
|
|
72
|
+
return resolved;
|
|
69
73
|
if (produceSsml && resolved.ssml && maxEntries > 1) {
|
|
70
74
|
setCached(cacheKeyFor(opts.language, false, mathSpeech, chunk.mathml), {
|
|
71
75
|
speechText: resolved.speechText,
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* without rewriting the core. The runtime binds live DOM via `DomAnchor`
|
|
15
15
|
* (see `./dom/types.ts`).
|
|
16
16
|
*/
|
|
17
|
-
export type StructuralBreakStrength = "weak" | "medium" | "strong";
|
|
18
17
|
export interface ProseSegment {
|
|
19
18
|
kind: "prose";
|
|
20
19
|
/** Spoken words for this prose run. Identical to `visibleText` today. */
|
|
@@ -53,18 +52,7 @@ export interface MathSegment {
|
|
|
53
52
|
*/
|
|
54
53
|
ssml?: string;
|
|
55
54
|
}
|
|
56
|
-
/**
|
|
57
|
-
* A structural pause. Breaks are never standalone speakable units: the DOM
|
|
58
|
-
* adapter folds them into the SSML of an adjacent speakable segment (or relies
|
|
59
|
-
* on the gap between sequential per-chunk utterances). Modeled here so the
|
|
60
|
-
* plan can carry pause intent for the SSML serializer.
|
|
61
|
-
*/
|
|
62
|
-
export interface StructuralBreak {
|
|
63
|
-
kind: "break";
|
|
64
|
-
strength: StructuralBreakStrength;
|
|
65
|
-
}
|
|
66
55
|
export type SpeechSegment = ProseSegment | MathSegment;
|
|
67
|
-
export type PlanSegment = SpeechSegment | StructuralBreak;
|
|
68
56
|
/** One assembled (still anchor-free) segment plus its source chunk index. */
|
|
69
57
|
export interface AssembledSegment {
|
|
70
58
|
segment: SpeechSegment;
|