@pie-players/pie-assessment-toolkit 0.3.72 → 0.3.74
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 +171 -56
- package/dist/attempt/AssessmentSession.d.ts +1 -5
- package/dist/attempt/AssessmentSession.js +0 -29
- 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 +14 -14
- package/dist/components/SectionToolBar.custom-element.d.ts +1 -0
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +40 -0
- package/dist/components/chunks/ItemToolBar-gmrryv2v.js +2 -0
- package/dist/components/chunks/ItemToolBar-h4xap0t0.js +4 -0
- package/dist/components/chunks/ItemToolBar-wr2myaah.js +2 -0
- package/dist/components/chunks/sre-engine-wf9s43gg.js +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +5 -0
- package/dist/context/runtime-context-consumer.js +5 -38
- package/dist/index.d.ts +6 -6
- package/dist/index.js +8 -9
- package/dist/policy/core/ToolPolicyEngine.d.ts +8 -2
- package/dist/policy/core/ToolPolicyEngine.js +12 -0
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +3 -3
- package/dist/runtime/adapter/SectionEngineAdapter.js +3 -3
- package/dist/runtime/registration-events.d.ts +3 -0
- package/dist/runtime/section-runtime-engine-host-context.d.ts +3 -9
- package/dist/runtime/section-runtime-engine-host-context.js +5 -43
- package/dist/runtime/tool-host-contract.d.ts +20 -0
- package/dist/runtime/tool-host-contract.js +21 -0
- package/dist/services/HighlightCoordinator.d.ts +1 -7
- package/dist/services/HighlightCoordinator.js +1 -3
- package/dist/services/TTSService.d.ts +15 -1
- package/dist/services/TTSService.js +88 -35
- package/dist/services/ToolCoordinator.d.ts +1 -7
- package/dist/services/ToolCoordinator.js +1 -3
- package/dist/services/ToolRegistry.d.ts +13 -6
- package/dist/services/ToolRegistry.js +19 -7
- package/dist/services/ToolkitCoordinator.d.ts +110 -18
- package/dist/services/ToolkitCoordinator.js +273 -85
- package/dist/services/catalog-owner.d.ts +11 -0
- package/dist/services/catalog-owner.js +25 -0
- package/dist/services/create-tools-config.d.ts +2 -2
- package/dist/services/framework-error.d.ts +8 -1
- package/dist/services/interfaces.d.ts +9 -9
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/section-controller-types.d.ts +30 -0
- package/dist/services/spoken-audio-cards.js +1 -1
- package/dist/services/tool-config-validation.d.ts +13 -2
- package/dist/services/tool-config-validation.js +33 -16
- package/dist/services/tool-context.js +29 -13
- package/dist/services/tool-providers/TTSToolProvider.d.ts +27 -65
- package/dist/services/tool-providers/TTSToolProvider.js +47 -9
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +4 -4
- package/dist/services/tool-providers/ToolProviderRegistry.js +4 -16
- package/dist/services/tool-providers/index.d.ts +0 -9
- package/dist/services/tool-providers/index.js +2 -4
- package/dist/services/tools-config-normalizer.d.ts +20 -1
- 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/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 +26 -1
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +3 -6
- 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.js +44 -2
- package/dist/services/tts/math-speech.d.ts +5 -1
- package/dist/services/tts/math-speech.js +9 -6
- 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/text-processing.d.ts +0 -1
- package/dist/services/tts/text-processing.js +0 -1
- package/dist/services/tts-runtime-config.d.ts +21 -3
- package/dist/services/tts-runtime-config.js +11 -3
- package/dist/tools/client.d.ts +1 -3
- package/dist/tools/client.js +1 -5
- package/dist/tools/internal.d.ts +6 -5
- package/dist/tools/internal.js +6 -8
- package/package.json +17 -36
- package/dist/components/chunks/ItemToolBar-byq80z42.js +0 -40
- package/dist/components/chunks/ItemToolBar-d36g3aw4.js +0 -49
- 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/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/index.d.ts +0 -8
- package/dist/tools/index.js +0 -10
- 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,3 +1,4 @@
|
|
|
1
|
+
import type { CalculatorProviderConfig } from "@pie-players/pie-calculator";
|
|
1
2
|
import type { SREMathSpeechOptions } from "./tts/math-speech.js";
|
|
2
3
|
export type ToolPlacementLevel = "section" | "item" | "passage";
|
|
3
4
|
export interface ToolPolicyConfig {
|
|
@@ -58,8 +59,19 @@ export interface TextToSpeechToolProviderConfig extends Omit<ToolProviderConfig,
|
|
|
58
59
|
mathSpeech?: SREMathSpeechOptions;
|
|
59
60
|
settings?: TextToSpeechToolProviderSettings;
|
|
60
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* `tools.providers.calculator`, closed to the keys the calculator reads, so a
|
|
64
|
+
* misplaced key such as a top-level `authFetcher` fails to compile instead of
|
|
65
|
+
* being ignored. `Pick` keeps it assignable to the providers index signature,
|
|
66
|
+
* which an interface would not be.
|
|
67
|
+
*/
|
|
68
|
+
export type CalculatorToolProviderConfig = Pick<CalculatorProviderConfig, keyof CalculatorProviderConfig> & {
|
|
69
|
+
enabled?: boolean;
|
|
70
|
+
provider?: ToolRuntimeProviderConfig;
|
|
71
|
+
};
|
|
61
72
|
export interface ToolProvidersConfig {
|
|
62
73
|
textToSpeech?: TextToSpeechToolProviderConfig;
|
|
74
|
+
calculator?: CalculatorToolProviderConfig;
|
|
63
75
|
[key: string]: ToolProviderConfig | TextToSpeechToolProviderConfig | undefined;
|
|
64
76
|
}
|
|
65
77
|
/**
|
|
@@ -89,7 +101,14 @@ export interface CanonicalToolsConfig {
|
|
|
89
101
|
*/
|
|
90
102
|
pnpEnforcement?: ToolsPnpEnforcement;
|
|
91
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* The tools config a host supplies. Every part is optional, placement levels
|
|
106
|
+
* included: {@link normalizeToolsConfig} fills a missing level with its default.
|
|
107
|
+
*/
|
|
108
|
+
export type ToolsConfigInput = Omit<Partial<CanonicalToolsConfig>, "placement"> & {
|
|
109
|
+
placement?: ToolPlacementConfig;
|
|
110
|
+
};
|
|
92
111
|
export declare function normalizeToolAlias(toolId: string): string;
|
|
93
112
|
export declare function normalizeToolList(toolIds: string[] | undefined | null): string[];
|
|
94
113
|
export declare function parseToolList(input: string | undefined | null): string[];
|
|
95
|
-
export declare function normalizeToolsConfig(input?:
|
|
114
|
+
export declare function normalizeToolsConfig(input?: ToolsConfigInput | null): CanonicalToolsConfig;
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
+
import { type SpeechSourceTokenization } from "./math-alignment/speech-tokenizer.js";
|
|
1
2
|
export type CatalogChunkPlaybackMode = "exact-word" | "anchor-span" | "region-fallback";
|
|
2
|
-
export type BoundaryOffsetMode = "plain-spoken-text" | "raw-ssml" | "unsupported";
|
|
3
|
-
export interface CatalogTextToken {
|
|
4
|
-
text: string;
|
|
5
|
-
normalized: string;
|
|
6
|
-
start: number;
|
|
7
|
-
end: number;
|
|
8
|
-
}
|
|
9
3
|
export interface CatalogSpanAnchor {
|
|
10
4
|
spokenStart: number;
|
|
11
5
|
spokenEnd: number;
|
|
@@ -14,25 +8,16 @@ export interface CatalogSpanAnchor {
|
|
|
14
8
|
score: number;
|
|
15
9
|
}
|
|
16
10
|
export interface CatalogSpanAlignment {
|
|
17
|
-
|
|
18
|
-
spokenText: string;
|
|
11
|
+
speech: SpeechSourceTokenization;
|
|
19
12
|
visibleText: string;
|
|
20
13
|
playbackMode: CatalogChunkPlaybackMode;
|
|
21
|
-
boundaryOffsetMode: BoundaryOffsetMode;
|
|
22
14
|
anchors: CatalogSpanAnchor[];
|
|
23
15
|
confidence: number;
|
|
24
|
-
spokenTokens: CatalogTextToken[];
|
|
25
|
-
visibleTokens: CatalogTextToken[];
|
|
26
|
-
rawToSpokenOffsetMap: Map<number, number>;
|
|
27
16
|
}
|
|
28
17
|
export declare const createCatalogSpanAlignment: (args: {
|
|
29
18
|
speechText: string;
|
|
30
19
|
visibleText: string;
|
|
31
20
|
}) => CatalogSpanAlignment;
|
|
32
|
-
export declare const resolveSpokenBoundaryOffset: (alignment: CatalogSpanAlignment, position: number, length?: number, boundaryWord?: string) => {
|
|
33
|
-
start: number;
|
|
34
|
-
length: number;
|
|
35
|
-
} | null;
|
|
36
21
|
export declare const resolveVisibleSpanForBoundary: (alignment: CatalogSpanAlignment, spokenOffset: number) => {
|
|
37
22
|
start: number;
|
|
38
23
|
end: number;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -1,43 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveBoundaryToSpeechToken, tokenizeSpeechSource, } from "../math-alignment/speech-tokenizer.js";
|
|
3
|
-
const normalizeBoundaryWord = (word) => {
|
|
4
|
-
if (!word || /^<[^>]+>$/.test(word.trim()))
|
|
5
|
-
return null;
|
|
6
|
-
return (tokenizeSpeechSource({ speechText: word }).tokens[0]?.normalized || null);
|
|
7
|
-
};
|
|
8
|
-
// Resolve a provider boundary for a chunk that carries no catalog span
|
|
9
|
-
// alignment — notably the generated-SSML math path, whose `speechText` is a
|
|
10
|
-
// raw `<speak>` document. The provider's offset is an index into `speechText`
|
|
11
|
-
// exactly as it was sent (which may include SSML tags), so we tokenize that
|
|
12
|
-
// same string and let the shared speech tokenizer translate the offset into the
|
|
13
|
-
// extracted spoken text. Returns the boundary in spoken-text space alongside
|
|
14
|
-
// that spoken text, so the downstream word/offset checks share one coordinate
|
|
15
|
-
// system. The previous "direct" path treated the raw-SSML offset as a
|
|
16
|
-
// spoken-text offset and rejected every math boundary (PIE-623 regression).
|
|
17
|
-
const speechSourceBoundary = (chunk, event) => {
|
|
18
|
-
const tokenization = tokenizeSpeechSource({ speechText: chunk.speechText });
|
|
19
|
-
if (!Number.isFinite(event.position)) {
|
|
20
|
-
return { boundary: null, spokenText: tokenization.spokenText };
|
|
21
|
-
}
|
|
22
|
-
const resolved = resolveBoundaryToSpeechToken({
|
|
23
|
-
tokenization,
|
|
24
|
-
position: event.position,
|
|
25
|
-
length: event.length,
|
|
26
|
-
boundaryWord: event.word,
|
|
27
|
-
});
|
|
28
|
-
return {
|
|
29
|
-
boundary: resolved
|
|
30
|
-
? { start: resolved.start, length: resolved.length }
|
|
31
|
-
: null,
|
|
32
|
-
spokenText: tokenization.spokenText,
|
|
33
|
-
};
|
|
34
|
-
};
|
|
35
|
-
const tokenAtOffsetMatches = (speechText, start, normalizedWord) => {
|
|
36
|
-
if (!normalizedWord)
|
|
37
|
-
return true;
|
|
38
|
-
const token = tokenizeSpeechSource({ speechText }).tokens.find((candidate) => candidate.start <= start && start < candidate.end);
|
|
39
|
-
return token?.normalized === normalizedWord;
|
|
40
|
-
};
|
|
1
|
+
import { normalizeBoundaryWord, resolveBoundaryToSpeechToken, tokenizeSpeechSource, } from "../math-alignment/speech-tokenizer.js";
|
|
41
2
|
export const normalizeBoundaryEvent = (chunk, event) => {
|
|
42
3
|
const normalizedWord = normalizeBoundaryWord(event.word);
|
|
43
4
|
if (chunk.id !== event.chunkId) {
|
|
@@ -61,17 +22,16 @@ export const normalizeBoundaryEvent = (chunk, event) => {
|
|
|
61
22
|
reason: "boundary offset space is unsupported",
|
|
62
23
|
};
|
|
63
24
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
25
|
+
// The provider's offset indexes `speechText` exactly as sent, which may be a
|
|
26
|
+
// raw `<speak>` document; the shared tokenizer maps it into spoken text and
|
|
27
|
+
// checks it against the reported word, on the catalog path and without one.
|
|
28
|
+
const boundary = resolveBoundaryToSpeechToken({
|
|
29
|
+
tokenization: chunk.catalogAlignment?.speech ??
|
|
30
|
+
tokenizeSpeechSource({ speechText: chunk.speechText }),
|
|
31
|
+
position: event.position,
|
|
32
|
+
length: event.length,
|
|
33
|
+
boundaryWord: event.word,
|
|
34
|
+
});
|
|
75
35
|
if (!boundary) {
|
|
76
36
|
return {
|
|
77
37
|
chunkId: chunk.id,
|
|
@@ -82,22 +42,12 @@ export const normalizeBoundaryEvent = (chunk, event) => {
|
|
|
82
42
|
reason: "could not resolve provider boundary to chunk speech",
|
|
83
43
|
};
|
|
84
44
|
}
|
|
85
|
-
if (!tokenAtOffsetMatches(spokenText, boundary.start, normalizedWord)) {
|
|
86
|
-
return {
|
|
87
|
-
chunkId: chunk.id,
|
|
88
|
-
normalizedWord,
|
|
89
|
-
chunkSpokenStart: null,
|
|
90
|
-
chunkSpokenEnd: null,
|
|
91
|
-
confidence: 0,
|
|
92
|
-
reason: "could not resolve provider boundary to matching word",
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
45
|
return {
|
|
96
46
|
chunkId: chunk.id,
|
|
97
47
|
normalizedWord,
|
|
98
48
|
chunkSpokenStart: boundary.start,
|
|
99
49
|
chunkSpokenEnd: boundary.start + boundary.length,
|
|
100
|
-
confidence:
|
|
50
|
+
confidence: boundary.confidence,
|
|
101
51
|
reason: chunk.catalogAlignment
|
|
102
52
|
? "resolved through catalog alignment"
|
|
103
53
|
: "resolved through speech tokenization",
|
|
@@ -3,6 +3,7 @@ import { createHighlightDecision } from "./highlight-policy.js";
|
|
|
3
3
|
import { resolveMathBoundaryTarget } from "./math-semantic-resolver.js";
|
|
4
4
|
import { resolveProseBoundaryTarget } from "./prose-boundary-resolver.js";
|
|
5
5
|
import { classifyMathHighlightCapability, createRenderedMathTargetResolver, } from "./rendered-math-target-resolver.js";
|
|
6
|
+
import { isNodeHiddenForTTS } from "../text-processing.js";
|
|
6
7
|
const regionTargetFor = (chunk) => chunk.regionRange
|
|
7
8
|
? {
|
|
8
9
|
type: "range",
|
|
@@ -73,6 +74,25 @@ const wouldEscalateInsideTokenMath = (planned, target, candidates) => {
|
|
|
73
74
|
element.contains(startContainer) &&
|
|
74
75
|
element.contains(endContainer));
|
|
75
76
|
};
|
|
77
|
+
// A prose range in hidden content paints nothing. Catalog words over MathJax
|
|
78
|
+
// math land in its clipped source MathML, so they select the equation, and a
|
|
79
|
+
// range over one source token resolves to that token's rendered glyph — the
|
|
80
|
+
// fill-in a single-text-node range gives native MathML.
|
|
81
|
+
const isHiddenRange = (target) => target?.type === "range" && isNodeHiddenForTTS(target.range.startContainer);
|
|
82
|
+
const renderHiddenProseTarget = (planned, target) => {
|
|
83
|
+
if (!isHiddenRange(target))
|
|
84
|
+
return null;
|
|
85
|
+
const { startContainer, endContainer, startOffset, endOffset } = target.range;
|
|
86
|
+
if (startContainer !== endContainer || startContainer.nodeType !== TEXT_NODE)
|
|
87
|
+
return null;
|
|
88
|
+
return planned.renderedMathTargetResolver({
|
|
89
|
+
type: "text-range",
|
|
90
|
+
quality: "exact-word",
|
|
91
|
+
node: startContainer,
|
|
92
|
+
startOffset,
|
|
93
|
+
endOffset,
|
|
94
|
+
});
|
|
95
|
+
};
|
|
76
96
|
const createPlannedChunk = (chunk, mathTokenHighlighting) => ({
|
|
77
97
|
chunk,
|
|
78
98
|
regionTarget: regionTargetFor(chunk),
|
|
@@ -136,9 +156,13 @@ export const createTTSHighlightPlan = (args) => {
|
|
|
136
156
|
const normalized = normalizeBoundaryEvent(chunk, event);
|
|
137
157
|
const proseTarget = resolveProseBoundaryTarget(chunk, normalized);
|
|
138
158
|
const mathCandidates = mathCandidatesForTarget(proseTarget, chunk);
|
|
139
|
-
const
|
|
159
|
+
const resolvedMathTarget = (!proseTarget || mathCandidates.length > 0
|
|
140
160
|
? resolveMathBoundaryTarget(chunk, normalized, event, mathCandidates, planned.renderedMathTargetResolver)
|
|
141
161
|
: null) || null;
|
|
162
|
+
const mathTarget = resolvedMathTarget?.quality === "semantic-token"
|
|
163
|
+
? resolvedMathTarget
|
|
164
|
+
: (renderHiddenProseTarget(planned, proseTarget) ??
|
|
165
|
+
resolvedMathTarget);
|
|
142
166
|
const mathTokenTarget = mathTokenHighlighting &&
|
|
143
167
|
mathTarget &&
|
|
144
168
|
mathTarget.quality !== "expression"
|
|
@@ -169,6 +193,7 @@ export const createTTSHighlightPlan = (args) => {
|
|
|
169
193
|
// highlighting is disabled, any prose range that lands inside a
|
|
170
194
|
// formula is likewise rejected so the formula stays a single block.
|
|
171
195
|
if (proseTarget &&
|
|
196
|
+
!isHiddenRange(proseTarget) &&
|
|
172
197
|
(mathTokenHighlighting
|
|
173
198
|
? !wouldEscalateInsideTokenMath(planned, proseTarget, mathCandidates)
|
|
174
199
|
: mathCandidates.length === 0)) {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
const chunkOffsetSpaceFor = (chunk) => {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (mathOffsetSpace)
|
|
6
|
-
return mathOffsetSpace;
|
|
7
|
-
return "plain-spoken-text";
|
|
2
|
+
return (chunk.alignment?.speech.boundaryOffsetSpace ??
|
|
3
|
+
chunk.mathAlignment?.speech.boundaryOffsetSpace ??
|
|
4
|
+
"plain-spoken-text");
|
|
8
5
|
};
|
|
9
6
|
const mathAlignmentsFor = (chunk) => {
|
|
10
7
|
if (chunk.mathAlignments?.length)
|
|
@@ -14,11 +14,6 @@ export declare const resolveHighlightTargetForBoundary: (alignment: MathAwareAli
|
|
|
14
14
|
length?: number;
|
|
15
15
|
boundaryWord?: string;
|
|
16
16
|
}) => HighlightTarget;
|
|
17
|
-
export declare const resolveMatchedHighlightTargetForBoundary: (alignment: MathAwareAlignment, args: {
|
|
18
|
-
position: number;
|
|
19
|
-
length?: number;
|
|
20
|
-
boundaryWord?: string;
|
|
21
|
-
}) => HighlightTarget | null;
|
|
22
17
|
export declare const resolveMatchedHighlightTargetForSpokenBoundary: (alignment: MathAwareAlignment, args: {
|
|
23
18
|
position: number;
|
|
24
19
|
length?: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tokenizeMathML } from "./mathml-tokenizer.js";
|
|
2
2
|
import { resolveMatchedHighlightTargetForBoundary as resolveMatchedTargetForBoundary, resolveHighlightTargetForBoundary as resolveTargetForBoundary, } from "./range-resolver.js";
|
|
3
3
|
import { alignSpeechToMath } from "./sequence-aligner.js";
|
|
4
|
-
import { resolveBoundaryToSpeechToken, resolveSpokenOffsetToSpeechToken, tokenizeSpeechSource, } from "./speech-tokenizer.js";
|
|
4
|
+
import { normalizeBoundaryWord, resolveBoundaryToSpeechToken, resolveSpokenOffsetToSpeechToken, tokenizeSpeechSource, } from "./speech-tokenizer.js";
|
|
5
5
|
export const createMathAwareAlignment = (args) => {
|
|
6
6
|
const speech = tokenizeSpeechSource({ speechText: args.speechText });
|
|
7
7
|
const math = tokenizeMathML(args.mathElement);
|
|
@@ -23,18 +23,6 @@ export const resolveHighlightTargetForBoundary = (alignment, args) => {
|
|
|
23
23
|
boundary,
|
|
24
24
|
});
|
|
25
25
|
};
|
|
26
|
-
export const resolveMatchedHighlightTargetForBoundary = (alignment, args) => {
|
|
27
|
-
const boundary = resolveBoundaryToSpeechToken({
|
|
28
|
-
tokenization: alignment.speech,
|
|
29
|
-
position: args.position,
|
|
30
|
-
length: args.length,
|
|
31
|
-
boundaryWord: args.boundaryWord,
|
|
32
|
-
});
|
|
33
|
-
return resolveMatchedTargetForBoundary({
|
|
34
|
-
alignment: alignment.result,
|
|
35
|
-
boundary,
|
|
36
|
-
});
|
|
37
|
-
};
|
|
38
26
|
export const resolveMatchedHighlightTargetForSpokenBoundary = (alignment, args) => {
|
|
39
27
|
const boundary = resolveSpokenOffsetToSpeechToken({
|
|
40
28
|
tokenization: alignment.speech,
|
|
@@ -48,18 +36,10 @@ export const resolveMatchedHighlightTargetForSpokenBoundary = (alignment, args)
|
|
|
48
36
|
});
|
|
49
37
|
};
|
|
50
38
|
export const resolveUniqueMathTargetForBoundaryWord = (alignment, args) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
const boundaryToken = tokenizeSpeechSource({
|
|
55
|
-
speechText: args.boundaryWord,
|
|
56
|
-
}).tokens[0];
|
|
57
|
-
if (!boundaryToken)
|
|
39
|
+
const normalized = normalizeBoundaryWord(args.boundaryWord);
|
|
40
|
+
if (!normalized)
|
|
58
41
|
return null;
|
|
59
|
-
const matches = alignment.math.tokens.filter((token) =>
|
|
60
|
-
|
|
61
|
-
return (token.normalized.toLowerCase() === normalized ||
|
|
62
|
-
token.spokenAliases.some((alias) => alias.toLowerCase() === normalized));
|
|
63
|
-
});
|
|
42
|
+
const matches = alignment.math.tokens.filter((token) => token.normalized.toLowerCase() === normalized ||
|
|
43
|
+
token.spokenAliases.some((alias) => alias.toLowerCase() === normalized));
|
|
64
44
|
return matches.length === 1 ? matches[0].target : null;
|
|
65
45
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BoundaryOffsetSpace, SpeechAlignmentToken } from "./types.js";
|
|
1
|
+
import type { AlignmentTextToken, BoundaryOffsetSpace, SpeechAlignmentToken } from "./types.js";
|
|
2
2
|
export interface SpeechSourceTokenization {
|
|
3
3
|
speechText: string;
|
|
4
4
|
spokenText: string;
|
|
@@ -14,9 +14,17 @@ export interface ResolvedSpeechBoundary {
|
|
|
14
14
|
length: number;
|
|
15
15
|
confidence: number;
|
|
16
16
|
}
|
|
17
|
+
export declare const tokenizeAlignmentText: (text: string) => AlignmentTextToken[];
|
|
17
18
|
export declare const tokenizeSpeechSource: (args: {
|
|
18
19
|
speechText: string;
|
|
19
20
|
}) => SpeechSourceTokenization;
|
|
21
|
+
/**
|
|
22
|
+
* A provider boundary word in the form spoken tokens compare in, or null when
|
|
23
|
+
* the word carries no token (empty, a control tag, punctuation). The word goes
|
|
24
|
+
* through the same SSML extraction as the chunk, so an entity-encoded word
|
|
25
|
+
* normalizes to the text it encodes.
|
|
26
|
+
*/
|
|
27
|
+
export declare const normalizeBoundaryWord: (word?: string) => string | null;
|
|
20
28
|
export declare const resolveBoundaryToSpeechToken: (args: {
|
|
21
29
|
tokenization: SpeechSourceTokenization;
|
|
22
30
|
position: number;
|