@pie-players/pie-assessment-toolkit 0.3.75 → 1.0.0-beta.1
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 +451 -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-asenkvx6.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 +32 -30
- package/dist/policy/core/decision-types.d.ts +51 -34
- 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 +7 -3
- package/dist/policy/core/pnp-policy-inputs.js +17 -33
- package/dist/policy/core/provenance.d.ts +2 -2
- package/dist/policy/core/tool-parameters.d.ts +12 -0
- package/dist/policy/core/tool-parameters.js +27 -0
- package/dist/policy/engine.d.ts +1 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +18 -24
- package/dist/policy/sources/PnpPolicySource.js +57 -29
- 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 +51 -57
- package/dist/services/TTSService.js +323 -284
- package/dist/services/ToolCoordinator.d.ts +6 -49
- package/dist/services/ToolCoordinator.js +25 -61
- package/dist/services/ToolRegistry.d.ts +83 -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 +51 -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 +39 -24
- package/dist/services/tool-context.js +158 -92
- 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 +2 -0
- package/dist/services/toolbar-decided-tools.js +2 -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 +13 -22
- 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 +7 -6
- 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
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { DEFAULT_TOOL_PLACEMENT } from "./tool-config-defaults.js";
|
|
2
|
+
import { isPlainRecord } from "@pie-players/pie-players-shared/object";
|
|
2
3
|
const DEFAULT_PLACEMENT = {
|
|
3
4
|
section: [...DEFAULT_TOOL_PLACEMENT.section],
|
|
4
5
|
item: [...DEFAULT_TOOL_PLACEMENT.item],
|
|
5
6
|
passage: [...DEFAULT_TOOL_PLACEMENT.passage],
|
|
6
7
|
};
|
|
7
|
-
function isPlainObject(value) {
|
|
8
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
-
}
|
|
10
8
|
function assertStringArray(value, fieldPath) {
|
|
11
9
|
if (value == null)
|
|
12
10
|
return [];
|
|
@@ -22,7 +20,7 @@ function assertStringArray(value, fieldPath) {
|
|
|
22
20
|
function assertPlacementConfig(value) {
|
|
23
21
|
if (value == null)
|
|
24
22
|
return undefined;
|
|
25
|
-
if (!
|
|
23
|
+
if (!isPlainRecord(value)) {
|
|
26
24
|
throw new Error('Invalid tools config at "placement": expected an object with section/item/passage arrays.');
|
|
27
25
|
}
|
|
28
26
|
return value;
|
|
@@ -30,7 +28,7 @@ function assertPlacementConfig(value) {
|
|
|
30
28
|
function assertPolicyConfig(value) {
|
|
31
29
|
if (value == null)
|
|
32
30
|
return undefined;
|
|
33
|
-
if (!
|
|
31
|
+
if (!isPlainRecord(value)) {
|
|
34
32
|
throw new Error('Invalid tools config at "policy": expected an object with allowed/blocked arrays.');
|
|
35
33
|
}
|
|
36
34
|
return value;
|
|
@@ -38,7 +36,7 @@ function assertPolicyConfig(value) {
|
|
|
38
36
|
function assertProviderConfig(providerId, value) {
|
|
39
37
|
if (value == null)
|
|
40
38
|
return undefined;
|
|
41
|
-
if (!
|
|
39
|
+
if (!isPlainRecord(value)) {
|
|
42
40
|
throw new Error(`Invalid tools config at "providers.${providerId}": expected an object.`);
|
|
43
41
|
}
|
|
44
42
|
const config = value;
|
|
@@ -49,12 +47,12 @@ function assertProviderConfig(providerId, value) {
|
|
|
49
47
|
}
|
|
50
48
|
if ("settings" in config &&
|
|
51
49
|
config.settings !== undefined &&
|
|
52
|
-
!
|
|
50
|
+
!isPlainRecord(config.settings)) {
|
|
53
51
|
throw new Error(`Invalid tools config at "providers.${providerId}.settings": expected an object.`);
|
|
54
52
|
}
|
|
55
53
|
if ("provider" in config &&
|
|
56
54
|
config.provider !== undefined &&
|
|
57
|
-
!
|
|
55
|
+
!isPlainRecord(config.provider)) {
|
|
58
56
|
throw new Error(`Invalid tools config at "providers.${providerId}.provider": expected an object.`);
|
|
59
57
|
}
|
|
60
58
|
return config;
|
|
@@ -62,7 +60,7 @@ function assertProviderConfig(providerId, value) {
|
|
|
62
60
|
function assertProvidersConfig(value) {
|
|
63
61
|
if (value == null)
|
|
64
62
|
return {};
|
|
65
|
-
if (!
|
|
63
|
+
if (!isPlainRecord(value)) {
|
|
66
64
|
throw new Error('Invalid tools config at "providers": expected an object keyed by tool id.');
|
|
67
65
|
}
|
|
68
66
|
const normalized = {};
|
|
@@ -71,6 +69,10 @@ function assertProvidersConfig(value) {
|
|
|
71
69
|
}
|
|
72
70
|
return normalized;
|
|
73
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Tool ids trimmed, with empty ones dropped and duplicates removed in
|
|
74
|
+
* first-seen order. Throws on a non-string entry.
|
|
75
|
+
*/
|
|
74
76
|
export function normalizeToolList(toolIds) {
|
|
75
77
|
if (!toolIds || toolIds.length === 0)
|
|
76
78
|
return [];
|
|
@@ -101,8 +103,18 @@ function assertPnpEnforcement(value, fieldPath) {
|
|
|
101
103
|
return value;
|
|
102
104
|
throw new Error(`Invalid tools config at "${fieldPath}": expected "on" or "off", got ${JSON.stringify(value)}.`);
|
|
103
105
|
}
|
|
106
|
+
/** A placement level's tool ids, or the level's default when it names none. */
|
|
107
|
+
function normalizePlacementLevel(placement, level) {
|
|
108
|
+
const toolIds = normalizeToolList(assertStringArray(placement?.[level], `placement.${level}`));
|
|
109
|
+
return toolIds.length ? toolIds : [...DEFAULT_PLACEMENT[level]];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Check a host's tools config shape, throwing at the first malformed field, and
|
|
113
|
+
* return it canonical: tool lists trimmed and deduplicated, and every placement
|
|
114
|
+
* level that names no tool filled with its default.
|
|
115
|
+
*/
|
|
104
116
|
export function normalizeToolsConfig(input) {
|
|
105
|
-
if (input != null && !
|
|
117
|
+
if (input != null && !isPlainRecord(input)) {
|
|
106
118
|
throw new Error('Invalid tools config: expected an object with "policy", "placement", and "providers".');
|
|
107
119
|
}
|
|
108
120
|
const policy = assertPolicyConfig(input?.policy);
|
|
@@ -115,15 +127,9 @@ export function normalizeToolsConfig(input) {
|
|
|
115
127
|
blocked: normalizeToolList(assertStringArray(policy?.blocked, "policy.blocked")),
|
|
116
128
|
},
|
|
117
129
|
placement: {
|
|
118
|
-
section:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
item: normalizeToolList(assertStringArray(placement?.item, "placement.item")).length
|
|
122
|
-
? normalizeToolList(assertStringArray(placement?.item, "placement.item"))
|
|
123
|
-
: [...DEFAULT_PLACEMENT.item],
|
|
124
|
-
passage: normalizeToolList(assertStringArray(placement?.passage, "placement.passage")).length
|
|
125
|
-
? normalizeToolList(assertStringArray(placement?.passage, "placement.passage"))
|
|
126
|
-
: [...DEFAULT_PLACEMENT.passage],
|
|
130
|
+
section: normalizePlacementLevel(placement, "section"),
|
|
131
|
+
item: normalizePlacementLevel(placement, "item"),
|
|
132
|
+
passage: normalizePlacementLevel(placement, "passage"),
|
|
127
133
|
},
|
|
128
134
|
providers,
|
|
129
135
|
};
|
|
@@ -3,16 +3,15 @@
|
|
|
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 type { ITTSProvider, ITTSProviderImplementation, TTSConfig, TTSProviderCapabilities } from "@pie-players/pie-tts";
|
|
8
|
+
import type { ToolkitTTSConfig } from "./provider-options.js";
|
|
10
9
|
/**
|
|
11
10
|
* The part of another provider's configuration that keeps its meaning on
|
|
12
11
|
* browser speech. Every path that replaces a provider with browser speech
|
|
13
12
|
* starts it with this.
|
|
14
13
|
*/
|
|
15
|
-
export declare const browserFallbackConfig: (config: Partial<
|
|
14
|
+
export declare const browserFallbackConfig: (config: Partial<ToolkitTTSConfig>) => Partial<ToolkitTTSConfig>;
|
|
16
15
|
/**
|
|
17
16
|
* The voice traits browser voice selection reads. A `SpeechSynthesisVoice` has
|
|
18
17
|
* them all; a host's own voice list may carry a subset.
|
|
@@ -51,8 +50,6 @@ export declare const waitForBrowserVoices: (synth: SpeechSynthesis, timeoutMs: n
|
|
|
51
50
|
*/
|
|
52
51
|
export declare class BrowserTTSProvider implements ITTSProvider {
|
|
53
52
|
readonly providerId = "browser";
|
|
54
|
-
readonly providerName = "Browser Speech Synthesis";
|
|
55
|
-
readonly version = "1.0.0";
|
|
56
53
|
initialize(config: TTSConfig): Promise<ITTSProviderImplementation>;
|
|
57
54
|
getCapabilities(): TTSProviderCapabilities;
|
|
58
55
|
destroy(): void;
|
|
@@ -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.
|
|
@@ -36,6 +37,8 @@ export const browserFallbackConfig = (config) => {
|
|
|
36
37
|
fallback.rate = config.rate;
|
|
37
38
|
if (config.pitch !== undefined)
|
|
38
39
|
fallback.pitch = config.pitch;
|
|
40
|
+
if (config.volume !== undefined)
|
|
41
|
+
fallback.volume = config.volume;
|
|
39
42
|
if (config.mathTokenHighlighting !== undefined) {
|
|
40
43
|
fallback.mathTokenHighlighting = config.mathTokenHighlighting;
|
|
41
44
|
}
|
|
@@ -192,14 +195,11 @@ const shouldAssignBrowserVoice = (voice) => !voice.default;
|
|
|
192
195
|
*/
|
|
193
196
|
export class BrowserTTSProvider {
|
|
194
197
|
providerId = "browser";
|
|
195
|
-
providerName = "Browser Speech Synthesis";
|
|
196
|
-
version = "1.0.0";
|
|
197
198
|
async initialize(config) {
|
|
198
199
|
// SSR guard
|
|
199
200
|
if (typeof window === "undefined") {
|
|
200
201
|
throw new Error("BrowserTTSProvider requires browser environment");
|
|
201
202
|
}
|
|
202
|
-
// Check browser support
|
|
203
203
|
if (!("speechSynthesis" in window)) {
|
|
204
204
|
throw new Error("Browser does not support Speech Synthesis API");
|
|
205
205
|
}
|
|
@@ -212,14 +212,9 @@ export class BrowserTTSProvider {
|
|
|
212
212
|
}
|
|
213
213
|
getCapabilities() {
|
|
214
214
|
return {
|
|
215
|
-
supportsPause: true,
|
|
216
|
-
supportsResume: true,
|
|
217
215
|
supportsWordBoundary: true,
|
|
218
216
|
// Boundary events depend on the voice: several network voices send none.
|
|
219
217
|
defaultHighlightMode: "sentence",
|
|
220
|
-
supportsVoiceSelection: true,
|
|
221
|
-
supportsRateControl: true,
|
|
222
|
-
supportsPitchControl: true,
|
|
223
218
|
// The Web Speech API voices plain text only; SSML tags are read aloud.
|
|
224
219
|
supportsSSML: false,
|
|
225
220
|
};
|
|
@@ -351,7 +346,6 @@ class BrowserTTSProviderImpl {
|
|
|
351
346
|
return runId === this.speakRunId;
|
|
352
347
|
}
|
|
353
348
|
splitIntoChunks(text) {
|
|
354
|
-
const MAX_CHUNK_LENGTH = 260;
|
|
355
349
|
if (text.length <= MAX_CHUNK_LENGTH) {
|
|
356
350
|
return [{ text, offset: 0 }];
|
|
357
351
|
}
|
|
@@ -408,7 +402,6 @@ class BrowserTTSProviderImpl {
|
|
|
408
402
|
: undefined;
|
|
409
403
|
return {
|
|
410
404
|
useSentenceSegmenter: useSegmenter,
|
|
411
|
-
useWordSegmenter: useSegmenter,
|
|
412
405
|
locale,
|
|
413
406
|
};
|
|
414
407
|
}
|
|
@@ -446,8 +439,10 @@ class BrowserTTSProviderImpl {
|
|
|
446
439
|
utterance.voice = voice;
|
|
447
440
|
if (this.config?.rate)
|
|
448
441
|
utterance.rate = this.config.rate;
|
|
449
|
-
if (this.config?.pitch)
|
|
442
|
+
if (this.config?.pitch !== undefined)
|
|
450
443
|
utterance.pitch = this.config.pitch;
|
|
444
|
+
if (this.config?.volume !== undefined)
|
|
445
|
+
utterance.volume = this.config.volume;
|
|
451
446
|
let didStart = false;
|
|
452
447
|
let settled = false;
|
|
453
448
|
let startTimeout = null;
|
|
@@ -558,7 +553,7 @@ class BrowserTTSProviderImpl {
|
|
|
558
553
|
const inferredLength = this.inferWordLength(chunkText, charIndex);
|
|
559
554
|
const wordLength = Number.isFinite(reportedLength) &&
|
|
560
555
|
reportedLength > 0 &&
|
|
561
|
-
reportedLength <=
|
|
556
|
+
reportedLength <= MAX_TRUSTED_REPORTED_WORD_LENGTH &&
|
|
562
557
|
charIndex + reportedLength <= chunkText.length
|
|
563
558
|
? reportedLength
|
|
564
559
|
: inferredLength;
|
|
@@ -624,27 +619,23 @@ class BrowserTTSProviderImpl {
|
|
|
624
619
|
this._isPaused = false;
|
|
625
620
|
this.releaseHold();
|
|
626
621
|
}
|
|
627
|
-
isPlaying() {
|
|
628
|
-
return this._isPlaying && !this._isPaused;
|
|
629
|
-
}
|
|
630
|
-
isPaused() {
|
|
631
|
-
return this._isPaused;
|
|
632
|
-
}
|
|
633
622
|
/**
|
|
634
|
-
* Update settings dynamically (rate, pitch, voice)
|
|
623
|
+
* Update settings dynamically (rate, pitch, volume, voice)
|
|
635
624
|
* Changes take effect on the next speak() call
|
|
636
625
|
*/
|
|
637
626
|
updateSettings(settings) {
|
|
638
627
|
if (!this.config) {
|
|
639
628
|
this.config = {};
|
|
640
629
|
}
|
|
641
|
-
// Update config with new settings
|
|
642
630
|
if (settings.rate !== undefined) {
|
|
643
631
|
this.config.rate = settings.rate;
|
|
644
632
|
}
|
|
645
633
|
if (settings.pitch !== undefined) {
|
|
646
634
|
this.config.pitch = settings.pitch;
|
|
647
635
|
}
|
|
636
|
+
if (settings.volume !== undefined) {
|
|
637
|
+
this.config.volume = settings.volume;
|
|
638
|
+
}
|
|
648
639
|
if (settings.voice !== undefined) {
|
|
649
640
|
this.config.voice = settings.voice;
|
|
650
641
|
}
|
|
@@ -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)
|