@pie-players/pie-assessment-toolkit 0.3.75 → 1.0.0-beta.0
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 +449 -541
- package/custom-elements.json +1315 -0
- package/dist/attempt/TestSession.d.ts +0 -1
- package/dist/attempt/TestSession.js +10 -5
- package/dist/components/ItemScope.custom-element.js +1 -1
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +8 -8
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/{ItemToolBar-7tt7v2xp.js → ItemToolBar-3zrc4p3d.js} +4 -4
- package/dist/components/chunks/{ItemToolBar-jx38w99s.js → ItemToolBar-66hcwmfw.js} +2 -2
- package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
- package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
- package/dist/components/chunks/{ItemToolBar-97hx60z4.js → ItemToolBar-kkj69ptr.js} +1 -1
- package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +6 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +4 -10
- package/dist/policy/core/PolicySource.d.ts +1 -1
- package/dist/policy/core/ToolPolicyEngine.d.ts +46 -7
- package/dist/policy/core/ToolPolicyEngine.js +73 -16
- package/dist/policy/core/compose-decision.d.ts +3 -3
- package/dist/policy/core/compose-decision.js +30 -30
- package/dist/policy/core/decision-types.d.ts +44 -32
- package/dist/policy/core/feature-decision.d.ts +17 -15
- package/dist/policy/core/feature-decision.js +11 -13
- package/dist/policy/core/pnp-policy-inputs.d.ts +5 -2
- package/dist/policy/core/pnp-policy-inputs.js +12 -32
- package/dist/policy/core/provenance.d.ts +1 -1
- package/dist/policy/core/tool-parameters.d.ts +11 -0
- package/dist/policy/core/tool-parameters.js +15 -0
- package/dist/policy/engine.d.ts +1 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +14 -22
- package/dist/policy/sources/PnpPolicySource.js +30 -28
- package/dist/runtime/SectionControllerBinding.d.ts +6 -2
- package/dist/runtime/SectionControllerBinding.js +21 -15
- package/dist/runtime/adapter/subscriber-fanout.d.ts +0 -1
- package/dist/runtime/adapter/subscriber-fanout.js +1 -4
- package/dist/runtime/composition-emit-scheduler.d.ts +3 -3
- package/dist/runtime/composition-emit-scheduler.js +1 -1
- package/dist/runtime/content-region.d.ts +0 -4
- package/dist/runtime/content-region.js +2 -2
- package/dist/runtime/core/SectionEngineCore.d.ts +0 -2
- package/dist/runtime/core/SectionEngineCore.js +0 -4
- package/dist/runtime/core/cohort.d.ts +0 -5
- package/dist/runtime/core/cohort.js +0 -9
- package/dist/runtime/core/engine-output.d.ts +3 -2
- package/dist/runtime/core/engine-transition.js +3 -2
- package/dist/runtime/registration-events.d.ts +11 -14
- package/dist/runtime/runtime-id.d.ts +1 -1
- package/dist/runtime/runtime-id.js +1 -1
- package/dist/runtime/shell-scope.d.ts +4 -4
- package/dist/runtime/shell-scope.js +15 -9
- package/dist/runtime/tool-host-contract.d.ts +0 -4
- package/dist/runtime/tool-host-contract.js +1 -1
- package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
- package/dist/services/AccessibilityCatalogResolver.js +29 -42
- package/dist/services/ElementToolStateStore.d.ts +32 -61
- package/dist/services/ElementToolStateStore.js +37 -67
- package/dist/services/HighlightCoordinator.d.ts +9 -21
- package/dist/services/HighlightCoordinator.js +52 -80
- package/dist/services/RangeSerializer.d.ts +4 -35
- package/dist/services/RangeSerializer.js +4 -48
- package/dist/services/SSMLExtractor.d.ts +8 -31
- package/dist/services/SSMLExtractor.js +18 -40
- package/dist/services/TTSService.d.ts +49 -56
- package/dist/services/TTSService.js +308 -283
- package/dist/services/ToolCoordinator.d.ts +6 -49
- package/dist/services/ToolCoordinator.js +25 -61
- package/dist/services/ToolRegistry.d.ts +82 -80
- package/dist/services/ToolRegistry.js +91 -67
- package/dist/services/ToolkitCoordinator.d.ts +83 -81
- package/dist/services/ToolkitCoordinator.js +186 -174
- package/dist/services/framework-error.d.ts +30 -7
- package/dist/services/framework-error.js +10 -13
- package/dist/services/interfaces.d.ts +47 -71
- package/dist/services/section-controller-types.d.ts +7 -7
- package/dist/services/section-session-assignment.js +6 -8
- package/dist/services/spoken-audio-cards.d.ts +2 -4
- package/dist/services/spoken-audio-cards.js +2 -4
- package/dist/services/tool-config-validation.d.ts +5 -7
- package/dist/services/tool-config-validation.js +11 -26
- package/dist/services/tool-context.d.ts +7 -23
- package/dist/services/tool-context.js +99 -53
- package/dist/services/tool-failure.d.ts +30 -0
- package/dist/services/tool-failure.js +11 -0
- package/dist/services/tool-icons.d.ts +1 -2
- package/dist/services/tool-icons.js +2 -2
- package/dist/services/tool-instance-id.d.ts +8 -6
- package/dist/services/tool-instance-id.js +12 -18
- package/dist/services/tool-providers/TTSToolProvider.d.ts +6 -43
- package/dist/services/tool-providers/TTSToolProvider.js +7 -63
- package/dist/services/tool-providers/ToolProviderApi.d.ts +0 -7
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +7 -42
- package/dist/services/tool-providers/ToolProviderRegistry.js +21 -63
- package/dist/services/tool-request.d.ts +4 -0
- package/dist/services/tool-request.js +7 -2
- package/dist/services/toolbar-decided-tools.d.ts +1 -0
- package/dist/services/toolbar-decided-tools.js +1 -0
- package/dist/services/toolbar-items.js +2 -5
- package/dist/services/tools-config-normalizer.d.ts +18 -9
- package/dist/services/tools-config-normalizer.js +25 -19
- package/dist/services/tts/browser-provider.d.ts +2 -5
- package/dist/services/tts/browser-provider.js +4 -20
- package/dist/services/tts/catalog-span-alignment.js +14 -7
- package/dist/services/tts/flat-tree.d.ts +0 -7
- package/dist/services/tts/flat-tree.js +2 -2
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
- package/dist/services/tts/generated-speech/index.d.ts +1 -1
- package/dist/services/tts/generated-speech/index.js +1 -1
- package/dist/services/tts/generated-speech/types.d.ts +1 -1
- package/dist/services/tts/generated-speech/types.js +1 -1
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +10 -12
- package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
- package/dist/services/tts/math-aware-text-processing.js +10 -29
- package/dist/services/tts/math-speech.d.ts +1 -1
- package/dist/services/tts/math-speech.js +3 -0
- package/dist/services/tts/mathml-sanitization.d.ts +7 -0
- package/dist/services/tts/mathml-sanitization.js +9 -1
- package/dist/services/tts/provider-options.d.ts +12 -1
- package/dist/services/tts/ssml/spoken-text.js +1 -1
- package/dist/services/tts/start-failure.d.ts +0 -7
- package/dist/services/tts/start-failure.js +1 -1
- package/dist/services/tts/text-segmentation.d.ts +5 -0
- package/dist/services/tts/text-segmentation.js +13 -10
- package/dist/services/tts-runtime-config.d.ts +32 -12
- package/dist/services/tts-runtime-config.js +14 -20
- package/dist/tools/content-capability-resolution.d.ts +2 -2
- package/dist/tools/registration.d.ts +6 -5
- package/dist/tools/registration.js +10 -9
- package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +6 -0
- package/dist/tools/tool-surface-host.js +1 -8
- package/package.json +8 -6
- package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +0 -2
- package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +0 -40
- package/dist/components/chunks/sre-engine-jd8cc08x.js +0 -1
- package/dist/services/ContextVariableStore.d.ts +0 -113
- package/dist/services/ContextVariableStore.js +0 -206
- package/dist/services/tts-control-events.d.ts +0 -13
- package/dist/services/tts-control-events.js +0 -7
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.js +0 -10
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Text-to-speech provider using the browser's Web Speech API.
|
|
5
5
|
* Works in all modern browsers that support SpeechSynthesis.
|
|
6
|
-
*
|
|
7
|
-
* Part of PIE Assessment Toolkit.
|
|
8
6
|
*/
|
|
9
7
|
import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
|
|
10
8
|
import { extractSpokenText, isSsmlDocument } from "./ssml/spoken-text.js";
|
|
@@ -12,6 +10,9 @@ import { segmentSentences as segmentTextToSentences } from "./text-segmentation.
|
|
|
12
10
|
const logger = createPieLogger("browser-tts-provider", isTtsDebugEnabled);
|
|
13
11
|
const NATIVE_START_TIMEOUT_MS = 5_000;
|
|
14
12
|
const VOICE_INVENTORY_TIMEOUT_MS = 2_000;
|
|
13
|
+
const MAX_CHUNK_LENGTH = 260;
|
|
14
|
+
// A longer reported word length falls back to the inferred one.
|
|
15
|
+
const MAX_TRUSTED_REPORTED_WORD_LENGTH = 80;
|
|
15
16
|
// The provider options the toolkit and this provider read. Every other option,
|
|
16
17
|
// and `voice`, belongs to the provider it was configured for: a Polly voice such
|
|
17
18
|
// as "Joanna" resolves to no browser voice, and an unresolved voice fails speak.
|
|
@@ -192,14 +193,11 @@ const shouldAssignBrowserVoice = (voice) => !voice.default;
|
|
|
192
193
|
*/
|
|
193
194
|
export class BrowserTTSProvider {
|
|
194
195
|
providerId = "browser";
|
|
195
|
-
providerName = "Browser Speech Synthesis";
|
|
196
|
-
version = "1.0.0";
|
|
197
196
|
async initialize(config) {
|
|
198
197
|
// SSR guard
|
|
199
198
|
if (typeof window === "undefined") {
|
|
200
199
|
throw new Error("BrowserTTSProvider requires browser environment");
|
|
201
200
|
}
|
|
202
|
-
// Check browser support
|
|
203
201
|
if (!("speechSynthesis" in window)) {
|
|
204
202
|
throw new Error("Browser does not support Speech Synthesis API");
|
|
205
203
|
}
|
|
@@ -212,14 +210,9 @@ export class BrowserTTSProvider {
|
|
|
212
210
|
}
|
|
213
211
|
getCapabilities() {
|
|
214
212
|
return {
|
|
215
|
-
supportsPause: true,
|
|
216
|
-
supportsResume: true,
|
|
217
213
|
supportsWordBoundary: true,
|
|
218
214
|
// Boundary events depend on the voice: several network voices send none.
|
|
219
215
|
defaultHighlightMode: "sentence",
|
|
220
|
-
supportsVoiceSelection: true,
|
|
221
|
-
supportsRateControl: true,
|
|
222
|
-
supportsPitchControl: true,
|
|
223
216
|
// The Web Speech API voices plain text only; SSML tags are read aloud.
|
|
224
217
|
supportsSSML: false,
|
|
225
218
|
};
|
|
@@ -351,7 +344,6 @@ class BrowserTTSProviderImpl {
|
|
|
351
344
|
return runId === this.speakRunId;
|
|
352
345
|
}
|
|
353
346
|
splitIntoChunks(text) {
|
|
354
|
-
const MAX_CHUNK_LENGTH = 260;
|
|
355
347
|
if (text.length <= MAX_CHUNK_LENGTH) {
|
|
356
348
|
return [{ text, offset: 0 }];
|
|
357
349
|
}
|
|
@@ -408,7 +400,6 @@ class BrowserTTSProviderImpl {
|
|
|
408
400
|
: undefined;
|
|
409
401
|
return {
|
|
410
402
|
useSentenceSegmenter: useSegmenter,
|
|
411
|
-
useWordSegmenter: useSegmenter,
|
|
412
403
|
locale,
|
|
413
404
|
};
|
|
414
405
|
}
|
|
@@ -558,7 +549,7 @@ class BrowserTTSProviderImpl {
|
|
|
558
549
|
const inferredLength = this.inferWordLength(chunkText, charIndex);
|
|
559
550
|
const wordLength = Number.isFinite(reportedLength) &&
|
|
560
551
|
reportedLength > 0 &&
|
|
561
|
-
reportedLength <=
|
|
552
|
+
reportedLength <= MAX_TRUSTED_REPORTED_WORD_LENGTH &&
|
|
562
553
|
charIndex + reportedLength <= chunkText.length
|
|
563
554
|
? reportedLength
|
|
564
555
|
: inferredLength;
|
|
@@ -624,12 +615,6 @@ class BrowserTTSProviderImpl {
|
|
|
624
615
|
this._isPaused = false;
|
|
625
616
|
this.releaseHold();
|
|
626
617
|
}
|
|
627
|
-
isPlaying() {
|
|
628
|
-
return this._isPlaying && !this._isPaused;
|
|
629
|
-
}
|
|
630
|
-
isPaused() {
|
|
631
|
-
return this._isPaused;
|
|
632
|
-
}
|
|
633
618
|
/**
|
|
634
619
|
* Update settings dynamically (rate, pitch, voice)
|
|
635
620
|
* Changes take effect on the next speak() call
|
|
@@ -638,7 +623,6 @@ class BrowserTTSProviderImpl {
|
|
|
638
623
|
if (!this.config) {
|
|
639
624
|
this.config = {};
|
|
640
625
|
}
|
|
641
|
-
// Update config with new settings
|
|
642
626
|
if (settings.rate !== undefined) {
|
|
643
627
|
this.config.rate = settings.rate;
|
|
644
628
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { tokenizeAlignmentText, tokenizeSpeechSource, } from "./math-alignment/speech-tokenizer.js";
|
|
2
2
|
import { normalizeTextForSpeech } from "./text-processing.js";
|
|
3
|
+
// A spoken token equal to a visible token; phrase targets score below it.
|
|
4
|
+
const EXACT_TOKEN_SCORE = 10;
|
|
5
|
+
// Below this, an anchored chunk highlights its region instead of spans.
|
|
6
|
+
const MIN_ANCHOR_SPAN_CONFIDENCE = 0.12;
|
|
3
7
|
// LIMITATION (i18n): the phrase table is English, so multi-word operator
|
|
4
8
|
// phrases ("plus or minus", "divided by") only align for English speech; other
|
|
5
9
|
// locales degrade to coarse region highlighting.
|
|
@@ -29,7 +33,7 @@ const createCandidates = (spokenTokens, visibleTokens) => {
|
|
|
29
33
|
spokenStartToken: spokenIndex,
|
|
30
34
|
spokenEndToken: spokenIndex + 1,
|
|
31
35
|
visibleToken: visibleIndex,
|
|
32
|
-
score:
|
|
36
|
+
score: EXACT_TOKEN_SCORE,
|
|
33
37
|
});
|
|
34
38
|
}
|
|
35
39
|
}
|
|
@@ -57,6 +61,9 @@ const createCandidates = (spokenTokens, visibleTokens) => {
|
|
|
57
61
|
}
|
|
58
62
|
return candidates;
|
|
59
63
|
};
|
|
64
|
+
// The highest-scoring chain of candidates monotonic in both spoken and visible
|
|
65
|
+
// order (O(n²) DP). A gap whose spoken and visible token counts differ by more
|
|
66
|
+
// than 2 pays the excess.
|
|
60
67
|
const computeAnchors = (spokenTokens, visibleTokens) => {
|
|
61
68
|
const candidates = createCandidates(spokenTokens, visibleTokens).sort((left, right) => spokenTokens[left.spokenStartToken].start -
|
|
62
69
|
spokenTokens[right.spokenStartToken].start ||
|
|
@@ -116,7 +123,7 @@ const scoreConfidence = (anchors, spokenTokens, visibleTokens) => {
|
|
|
116
123
|
Math.max(1, Math.min(spokenTokens.length, visibleTokens.length));
|
|
117
124
|
const averageScore = anchors.reduce((total, anchor) => total + anchor.score, 0) /
|
|
118
125
|
anchors.length /
|
|
119
|
-
|
|
126
|
+
EXACT_TOKEN_SCORE;
|
|
120
127
|
return Math.min(1, tokenCoverage * averageScore);
|
|
121
128
|
};
|
|
122
129
|
export const createCatalogSpanAlignment = (args) => {
|
|
@@ -130,17 +137,17 @@ export const createCatalogSpanAlignment = (args) => {
|
|
|
130
137
|
const confidence = scoreConfidence(anchors, spokenTokens, visibleTokens);
|
|
131
138
|
const isExact = speech.spokenText === visibleText && speech.spokenText.length > 0;
|
|
132
139
|
const hasShortSingleExactAnchor = anchors.length === 1 &&
|
|
133
|
-
anchors[0].score ===
|
|
140
|
+
anchors[0].score === EXACT_TOKEN_SCORE &&
|
|
134
141
|
spokenTokens.length <= 2 &&
|
|
135
142
|
visibleTokens.length <= 2;
|
|
143
|
+
const anchorsAreTrusted = (anchors.length >= 2 || hasShortSingleExactAnchor) &&
|
|
144
|
+
confidence >= MIN_ANCHOR_SPAN_CONFIDENCE;
|
|
136
145
|
const playbackMode = speech.unsupportedSemantic
|
|
137
146
|
? "region-fallback"
|
|
138
147
|
: isExact
|
|
139
148
|
? "exact-word"
|
|
140
|
-
:
|
|
141
|
-
?
|
|
142
|
-
? "anchor-span"
|
|
143
|
-
: "region-fallback"
|
|
149
|
+
: anchorsAreTrusted
|
|
150
|
+
? "anchor-span"
|
|
144
151
|
: "region-fallback";
|
|
145
152
|
return {
|
|
146
153
|
speech,
|
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
* global, at import or at call time.
|
|
18
18
|
*/
|
|
19
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
20
|
/** `node`'s children in the flat tree, with skipped elements left out. */
|
|
23
21
|
export declare const flatTreeChildNodes: (node: Node) => Node[];
|
|
24
22
|
/**
|
|
@@ -35,11 +33,6 @@ export declare const flatTextContent: (root: Node) => string;
|
|
|
35
33
|
/** Elements under `root` matching `selector`, in rendering order. */
|
|
36
34
|
export declare const flatQuerySelectorAll: (root: Node, selector: string) => Element[];
|
|
37
35
|
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
36
|
export declare const flatTreeParentElement: (node: Node) => Element | null;
|
|
44
37
|
/** The nearest element at or above `node` in the flat tree that matches `selector`. */
|
|
45
38
|
export declare const flatTreeClosest: (node: Node, selector: string) => Element | null;
|
|
@@ -35,7 +35,7 @@ export const isShadowRootNode = (node) => !!node &&
|
|
|
35
35
|
node.nodeType === DOCUMENT_FRAGMENT_NODE &&
|
|
36
36
|
node.host?.nodeType === ELEMENT_NODE;
|
|
37
37
|
/** Toolbars, tool elements and debug panels: chrome around content, never content. */
|
|
38
|
-
|
|
38
|
+
const isToolkitChromeElement = (element) => {
|
|
39
39
|
const tag = element.localName?.toLowerCase() || "";
|
|
40
40
|
if (CHROME_TAGS.has(tag))
|
|
41
41
|
return true;
|
|
@@ -143,7 +143,7 @@ export const flatQuerySelector = (root, selector) => {
|
|
|
143
143
|
* `node`'s parent in the flat tree: the slot rendering it, else its parent, with
|
|
144
144
|
* a shadow root standing for its host.
|
|
145
145
|
*/
|
|
146
|
-
|
|
146
|
+
const flatTreeParentNode = (node) => {
|
|
147
147
|
const slot = node.assignedSlot;
|
|
148
148
|
if (slot)
|
|
149
149
|
return slot;
|
|
@@ -3,8 +3,8 @@ import type { DomAnchor, GeneratedSpeechChunk } from "./types.js";
|
|
|
3
3
|
export interface PlanToChunksOptions {
|
|
4
4
|
/**
|
|
5
5
|
* Playback format:
|
|
6
|
-
* - "plain" (default): plain spoken text for every chunk (
|
|
7
|
-
*
|
|
6
|
+
* - "plain" (default): plain spoken text for every chunk (the stable
|
|
7
|
+
* generated-speech behavior).
|
|
8
8
|
* - "ssml": math chunks with SRE SSML send that SSML to the provider (with
|
|
9
9
|
* a plain fallback attached); prose chunks always stay plain.
|
|
10
10
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Generated-speech plan model (PURE core).
|
|
3
3
|
*
|
|
4
4
|
* This module is the speech-composition core for items that contain math but
|
|
5
|
-
* ship without authored SSML / `accessibilityCatalogs
|
|
5
|
+
* ship without authored SSML / `accessibilityCatalogs`. It is
|
|
6
6
|
* deliberately free of any imports from `TTSService`, the highlight renderer,
|
|
7
7
|
* `HighlightCoordinator`, Svelte, or custom-element entrypoints
|
|
8
8
|
* (`scripts/check-speech-composition-purity.mjs` enforces this). DOM primitive
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Generated-speech plan model (PURE core).
|
|
3
3
|
*
|
|
4
4
|
* This module is the speech-composition core for items that contain math but
|
|
5
|
-
* ship without authored SSML / `accessibilityCatalogs
|
|
5
|
+
* ship without authored SSML / `accessibilityCatalogs`. It is
|
|
6
6
|
* deliberately free of any imports from `TTSService`, the highlight renderer,
|
|
7
7
|
* `HighlightCoordinator`, Svelte, or custom-element entrypoints
|
|
8
8
|
* (`scripts/check-speech-composition-purity.mjs` enforces this). DOM primitive
|
|
@@ -67,11 +67,10 @@ const TEXT_NODE = 3;
|
|
|
67
67
|
// the last token — painting it would be a likely false positive, and "false
|
|
68
68
|
// positives are worse than coarse highlighting." A single-text-node prose range
|
|
69
69
|
// is kept: it underlines one token cleanly and usefully fills in any token the
|
|
70
|
-
// math resolver missed.
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
const wouldEscalateInsideTokenMath = (planned, target, candidates) => {
|
|
70
|
+
// math resolver missed. The word layer never escalates a range to the enclosing
|
|
71
|
+
// <math>/<mjx-container> (HighlightCoordinator WORD_FALLBACK_SELECTOR), so this
|
|
72
|
+
// is a precision policy.
|
|
73
|
+
const isMultiNodeRangeInsideTokenMath = (planned, target, candidates) => {
|
|
75
74
|
if (!target || target.type !== "range")
|
|
76
75
|
return false;
|
|
77
76
|
const { startContainer, endContainer } = target.range;
|
|
@@ -128,8 +127,7 @@ export const createTTSHighlightPlan = (args) => {
|
|
|
128
127
|
const planned = plannedChunksById.get(chunkId);
|
|
129
128
|
if (!planned)
|
|
130
129
|
return missingChunkDecision();
|
|
131
|
-
const
|
|
132
|
-
const soleElement = alignments.length === 1 ? alignments[0].element : null;
|
|
130
|
+
const soleElement = soleEquationElement(planned.chunk.mathAlignments);
|
|
133
131
|
const capability = soleElement
|
|
134
132
|
? planned.mathCapabilityByElement.get(soleElement)
|
|
135
133
|
: null;
|
|
@@ -164,9 +162,9 @@ export const createTTSHighlightPlan = (args) => {
|
|
|
164
162
|
const normalized = normalizeBoundaryEvent(chunk, event);
|
|
165
163
|
const proseTarget = resolveProseBoundaryTarget(chunk, normalized);
|
|
166
164
|
const mathCandidates = mathCandidatesForTarget(proseTarget, chunk);
|
|
167
|
-
const resolvedMathTarget =
|
|
165
|
+
const resolvedMathTarget = !proseTarget || mathCandidates.length > 0
|
|
168
166
|
? resolveMathBoundaryTarget(chunk, normalized, event, mathCandidates, planned.renderedMathTargetResolver)
|
|
169
|
-
: null
|
|
167
|
+
: null;
|
|
170
168
|
const mathTarget = resolvedMathTarget?.quality === "semantic-token"
|
|
171
169
|
? resolvedMathTarget
|
|
172
170
|
: (renderHiddenProseTarget(planned, proseTarget) ??
|
|
@@ -195,15 +193,15 @@ export const createTTSHighlightPlan = (args) => {
|
|
|
195
193
|
}
|
|
196
194
|
// (2) A spoken word that maps to visible prose. The prose anchor is
|
|
197
195
|
// precise; a coarse math expression fallback must never override it.
|
|
198
|
-
// Exception: a multi-node prose range inside a token-mode equation
|
|
199
|
-
//
|
|
196
|
+
// Exception: a multi-node prose range inside a token-mode equation is
|
|
197
|
+
// an untrustworthy glyph-level match, so it is rejected here and
|
|
200
198
|
// falls through to the token hold below. When per-token math
|
|
201
199
|
// highlighting is disabled, any prose range that lands inside a
|
|
202
200
|
// formula is likewise rejected so the formula stays a single block.
|
|
203
201
|
if (proseTarget &&
|
|
204
202
|
!isHiddenRange(proseTarget) &&
|
|
205
203
|
(mathTokenHighlighting
|
|
206
|
-
? !
|
|
204
|
+
? !isMultiNodeRangeInsideTokenMath(planned, proseTarget, mathCandidates)
|
|
207
205
|
: mathCandidates.length === 0)) {
|
|
208
206
|
return createHighlightDecision({
|
|
209
207
|
semanticTarget: proseTarget,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the host knows about the content being highlighted. `scopeElement`
|
|
3
|
+
* falls back to the content element being read.
|
|
4
|
+
*/
|
|
1
5
|
export interface TTSHighlightContext {
|
|
2
6
|
scopeElement?: HTMLElement | null;
|
|
3
7
|
itemId?: string;
|
|
@@ -6,12 +10,20 @@ export interface TTSHighlightContext {
|
|
|
6
10
|
contentKind?: string;
|
|
7
11
|
regionPolicy?: string;
|
|
8
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Host remap of TTS highlight targets. A null or undefined result keeps the
|
|
15
|
+
* native targets, and so does a result outside `scopeElement` or a throw. For
|
|
16
|
+
* sentences, one out-of-scope or non-Range/Element entry keeps all native
|
|
17
|
+
* ranges.
|
|
18
|
+
*/
|
|
9
19
|
export interface TTSHighlightTargetResolver {
|
|
10
20
|
resolveWordRange?(range: Range, context: TTSHighlightContext): Range | null | undefined;
|
|
11
21
|
resolveSentenceRanges?(ranges: Range[], context: TTSHighlightContext): Array<Range | HTMLElement> | null | undefined;
|
|
12
22
|
}
|
|
23
|
+
/** A resolver with the context it is called with. */
|
|
13
24
|
export interface TTSHighlightTargetResolverRuntime {
|
|
14
25
|
context: TTSHighlightContext;
|
|
15
26
|
resolver?: TTSHighlightTargetResolver | null;
|
|
16
27
|
}
|
|
28
|
+
/** Read at every highlight; a throw counts as no resolver. */
|
|
17
29
|
export type TTSHighlightTargetResolverProvider = () => TTSHighlightTargetResolverRuntime | null | undefined;
|
|
@@ -74,18 +74,16 @@ const TEXT_CHUNK_SOURCE_ROLES = new Set([
|
|
|
74
74
|
]);
|
|
75
75
|
const resolveTextChunkSourceElement = (textNode, root) => {
|
|
76
76
|
let current = flatTreeParentElement(textNode);
|
|
77
|
-
let best = null;
|
|
78
77
|
while (current && current !== root) {
|
|
79
78
|
const role = (current.getAttribute("role") || "").toLowerCase();
|
|
80
79
|
const tagName = current.tagName.toUpperCase();
|
|
81
80
|
if (TEXT_CHUNK_SOURCE_TAGS.has(tagName) ||
|
|
82
81
|
TEXT_CHUNK_SOURCE_ROLES.has(role)) {
|
|
83
|
-
|
|
84
|
-
break;
|
|
82
|
+
return current;
|
|
85
83
|
}
|
|
86
84
|
current = flatTreeParentElement(current);
|
|
87
85
|
}
|
|
88
|
-
return
|
|
86
|
+
return root;
|
|
89
87
|
};
|
|
90
88
|
const hasMathCandidate = (element) => Boolean(typeof element.querySelector === "function" &&
|
|
91
89
|
flatQuerySelector(element, "math, [data-mathml], .MathJax, mjx-container, mjx-assistive-mml")) ||
|
|
@@ -158,13 +156,12 @@ const appendCollectedText = (acc, collected) => {
|
|
|
158
156
|
for (let i = 0; i < collected.text.length; i++) {
|
|
159
157
|
const character = collected.text[i];
|
|
160
158
|
const isWhitespace = /\s/.test(character);
|
|
161
|
-
const mapping = collected.map.get(i)
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
const normalizedMapping = mapping
|
|
159
|
+
const mapping = collected.map.get(i);
|
|
160
|
+
// An unmapped character borrows the last mapped position.
|
|
161
|
+
const characterMapping = mapping
|
|
165
162
|
? { node: mapping.node, offset: mapping.offset }
|
|
166
|
-
:
|
|
167
|
-
? { node:
|
|
163
|
+
: acc.lastMapped
|
|
164
|
+
? { node: acc.lastMapped.node, offset: acc.lastMapped.offset }
|
|
168
165
|
: null;
|
|
169
166
|
if (acc.inLeadingWhitespace) {
|
|
170
167
|
if (isWhitespace)
|
|
@@ -173,29 +170,16 @@ const appendCollectedText = (acc, collected) => {
|
|
|
173
170
|
}
|
|
174
171
|
if (isWhitespace) {
|
|
175
172
|
if (!acc.lastCharWasWhitespace) {
|
|
176
|
-
appendCharacter(acc, " ",
|
|
173
|
+
appendCharacter(acc, " ", characterMapping);
|
|
177
174
|
}
|
|
178
175
|
acc.lastCharWasWhitespace = true;
|
|
179
176
|
continue;
|
|
180
177
|
}
|
|
181
178
|
if (i === 0 && shouldInsertBoundarySpace(acc, character)) {
|
|
182
|
-
appendCharacter(acc, " ",
|
|
179
|
+
appendCharacter(acc, " ", characterMapping);
|
|
183
180
|
}
|
|
184
|
-
appendCharacter(acc, character,
|
|
181
|
+
appendCharacter(acc, character, characterMapping);
|
|
185
182
|
acc.lastCharWasWhitespace = false;
|
|
186
|
-
if (mapping) {
|
|
187
|
-
acc.map.set(acc.position - 1, {
|
|
188
|
-
node: mapping.node,
|
|
189
|
-
offset: mapping.offset,
|
|
190
|
-
});
|
|
191
|
-
acc.lastMapped = { node: mapping.node, offset: mapping.offset };
|
|
192
|
-
}
|
|
193
|
-
else if (syntheticNode) {
|
|
194
|
-
acc.map.set(acc.position - 1, {
|
|
195
|
-
node: syntheticNode,
|
|
196
|
-
offset: syntheticOffset,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
183
|
}
|
|
200
184
|
};
|
|
201
185
|
const textFallbackFromMathML = (mathml) => {
|
|
@@ -376,9 +360,6 @@ const collectMathAware = (root, options, range) => {
|
|
|
376
360
|
trimTrailingWhitespace(acc);
|
|
377
361
|
flushTextChunk();
|
|
378
362
|
const visibleText = acc.chars.join("");
|
|
379
|
-
while (acc.map.has(visibleText.length)) {
|
|
380
|
-
acc.map.delete(visibleText.length);
|
|
381
|
-
}
|
|
382
363
|
return {
|
|
383
364
|
visibleText,
|
|
384
365
|
map: acc.map,
|
|
@@ -14,7 +14,7 @@ export interface ResolveMathSpeechOptions {
|
|
|
14
14
|
mathSpeech?: SREMathSpeechOptions;
|
|
15
15
|
/**
|
|
16
16
|
* Also produce SRE's SSML rendering (markup: "ssml") for the equation, for
|
|
17
|
-
* the runtime generated-SSML path
|
|
17
|
+
* the runtime generated-SSML path. Only populated when the input
|
|
18
18
|
* is a single math chunk and SRE succeeds. The plain `speechText` is always
|
|
19
19
|
* computed with markup: "none" so the plain path is unaffected.
|
|
20
20
|
*/
|
|
@@ -3,6 +3,9 @@ import { setSreStartupLocaleSource, sreLocaleSource } from "./sre-locales.js";
|
|
|
3
3
|
import { normalizeTextForSpeech } from "./text-processing.js";
|
|
4
4
|
// Debug lines need PIE_TTS_DEBUG=1 or globalThis.__PIE_TTS_DEBUG__ = true.
|
|
5
5
|
const logger = createPieLogger("tts-math-speech", isTtsDebugEnabled);
|
|
6
|
+
// SRE configures one global engine, so each setupEngine + toSpeech pair runs to
|
|
7
|
+
// completion before the next starts; concurrent reads would otherwise speak
|
|
8
|
+
// under each other's settings.
|
|
6
9
|
let sreOperationQueue = Promise.resolve();
|
|
7
10
|
const normalizeLocale = (language) => (language || "en").split("-")[0].toLowerCase() || "en";
|
|
8
11
|
/**
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MathML re-serialized from an allow-list, with a MathML xmlns on the root.
|
|
3
|
+
* Null for empty or unparseable input, no DOMParser, a non-<math> root, a
|
|
4
|
+
* disallowed element or a node other than element/text/comment, or an xmlns
|
|
5
|
+
* other than MathML's. Comments, attributes off the allow-list and values
|
|
6
|
+
* containing javascript: or data:text/html are dropped.
|
|
7
|
+
*/
|
|
1
8
|
export declare const canonicalizeMathML: (input: string) => string | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
|
|
2
|
+
const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
2
3
|
const ALLOWED_MATHML_ELEMENTS = new Set([
|
|
3
4
|
"math",
|
|
4
5
|
"maction",
|
|
@@ -109,7 +110,7 @@ const isAllowedMathElement = (element) => {
|
|
|
109
110
|
const namespace = element.namespaceURI;
|
|
110
111
|
return (!namespace ||
|
|
111
112
|
namespace === MATHML_NAMESPACE ||
|
|
112
|
-
namespace ===
|
|
113
|
+
namespace === XHTML_NAMESPACE);
|
|
113
114
|
};
|
|
114
115
|
const serializeMathNode = (node) => {
|
|
115
116
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
@@ -153,6 +154,13 @@ const parseMathML = (input) => {
|
|
|
153
154
|
}
|
|
154
155
|
return null;
|
|
155
156
|
};
|
|
157
|
+
/**
|
|
158
|
+
* MathML re-serialized from an allow-list, with a MathML xmlns on the root.
|
|
159
|
+
* Null for empty or unparseable input, no DOMParser, a non-<math> root, a
|
|
160
|
+
* disallowed element or a node other than element/text/comment, or an xmlns
|
|
161
|
+
* other than MathML's. Comments, attributes off the allow-list and values
|
|
162
|
+
* containing javascript: or data:text/html are dropped.
|
|
163
|
+
*/
|
|
156
164
|
export const canonicalizeMathML = (input) => {
|
|
157
165
|
const trimmed = String(input || "").trim();
|
|
158
166
|
if (!trimmed)
|
|
@@ -7,7 +7,11 @@ import type { BoundarySpacingMode } from "./text-processing.js";
|
|
|
7
7
|
* each value at runtime.
|
|
8
8
|
*/
|
|
9
9
|
export interface ToolkitTTSProviderOptions extends TTSProviderOptions {
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Highlight words or sentences while reading. Defaults by provider
|
|
12
|
+
* capability. A playback that sends no word boundaries highlights its
|
|
13
|
+
* sentence in either mode.
|
|
14
|
+
*/
|
|
11
15
|
highlightMode?: "word" | "sentence";
|
|
12
16
|
/** Locale for text processing; the toolkit sets it per speak. */
|
|
13
17
|
locale?: string;
|
|
@@ -38,4 +42,11 @@ export interface ToolkitTTSProviderOptions extends TTSProviderOptions {
|
|
|
38
42
|
/** A provider configuration as the toolkit reads it. */
|
|
39
43
|
export interface ToolkitTTSConfig extends TTSConfig {
|
|
40
44
|
providerOptions?: ToolkitTTSProviderOptions;
|
|
45
|
+
/**
|
|
46
|
+
* Highlight math token by token; `false` highlights each formula as one
|
|
47
|
+
* block. The highlight pipeline reads it, and providers ignore it.
|
|
48
|
+
*
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
mathTokenHighlighting?: boolean;
|
|
41
52
|
}
|
|
@@ -8,7 +8,7 @@ import { normalizeTextForSpeech } from "../text-processing.js";
|
|
|
8
8
|
* stripping used by:
|
|
9
9
|
* - the catalog span aligner (`catalog-span-alignment.ts`),
|
|
10
10
|
* - the math speech tokenizer (`math-alignment/speech-tokenizer.ts`),
|
|
11
|
-
* - the runtime generated-speech path
|
|
11
|
+
* - the runtime generated-speech path.
|
|
12
12
|
*
|
|
13
13
|
* Keeping one implementation guarantees provider word-boundary offsets (which
|
|
14
14
|
* are into the raw SSML string) map to the same spoken text on every path.
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `code` a speak's rejection carries when speech could not start, as
|
|
3
|
-
* distinct from a failure while speaking. Read by shape: several copies of the
|
|
4
|
-
* toolkit can share a page, so an error class from one fails `instanceof` in
|
|
5
|
-
* another.
|
|
6
|
-
*/
|
|
7
|
-
export declare const TTS_START_FAILED_CODE = "tts-start-failed";
|
|
8
1
|
/** Whether a speak rejected because speech could not start. */
|
|
9
2
|
export declare function isTTSStartFailure(error: unknown): boolean;
|
|
10
3
|
/** Marks `cause` as a start failure, keeping its message and stack. */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* toolkit can share a page, so an error class from one fails `instanceof` in
|
|
5
5
|
* another.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
const TTS_START_FAILED_CODE = "tts-start-failed";
|
|
8
8
|
/** Whether a speak rejected because speech could not start. */
|
|
9
9
|
export function isTTSStartFailure(error) {
|
|
10
10
|
return (typeof error === "object" &&
|
|
@@ -6,6 +6,11 @@ export interface SentenceSegmentationOptions {
|
|
|
6
6
|
locale?: string;
|
|
7
7
|
useSentenceSegmenter?: boolean;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* `text` split into sentences at their offsets in it, by Intl.Segmenter unless
|
|
11
|
+
* the policy disables it or it is missing. The segmenter path drops
|
|
12
|
+
* whitespace-only segments; the regex fallback keeps every match.
|
|
13
|
+
*/
|
|
9
14
|
export declare const segmentSentences: (text: string, options?: SentenceSegmentationOptions) => SentenceSegment[];
|
|
10
15
|
/**
|
|
11
16
|
* `text` in pieces of at most `maxLength` characters: whole sentences while they
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text` split into sentences at their offsets in it, by Intl.Segmenter unless
|
|
3
|
+
* the policy disables it or it is missing. The segmenter path drops
|
|
4
|
+
* whitespace-only segments; the regex fallback keeps every match.
|
|
5
|
+
*/
|
|
1
6
|
export const segmentSentences = (text, options) => {
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const Segmenter = globalThis.Intl?.Segmenter;
|
|
8
|
-
if (typeof Segmenter === "function") {
|
|
7
|
+
const Segmenter = options?.useSentenceSegmenter === false
|
|
8
|
+
? undefined
|
|
9
|
+
: globalThis.Intl?.Segmenter;
|
|
10
|
+
if (typeof Segmenter === "function") {
|
|
11
|
+
try {
|
|
9
12
|
const segmenter = new Segmenter(options?.locale, {
|
|
10
13
|
granularity: "sentence",
|
|
11
14
|
});
|
|
@@ -18,9 +21,9 @@ export const segmentSentences = (text, options) => {
|
|
|
18
21
|
if (parsed.length > 0)
|
|
19
22
|
return parsed;
|
|
20
23
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
catch {
|
|
25
|
+
// Fall through to regex segmentation.
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
const sentenceRegex = /[^.!?]+(?:[.!?]+|$)/g;
|
|
26
29
|
const raw = text.match(sentenceRegex) || [text];
|