@pie-players/pie-assessment-toolkit 0.3.44 → 0.3.45

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.
Files changed (191) hide show
  1. package/README.md +54 -69
  2. package/dist/components/ItemToolBar.custom-element.js +479 -324
  3. package/dist/components/PieAssessmentToolkit.custom-element.js +19498 -3590
  4. package/dist/components/SectionToolBar.custom-element.js +761 -507
  5. package/dist/index.d.ts +3 -6
  6. package/dist/index.js +1 -3
  7. package/dist/index.js.map +1 -1
  8. package/dist/policy/core/ToolPolicyEngine.d.ts +1 -2
  9. package/dist/policy/core/ToolPolicyEngine.js +1 -2
  10. package/dist/policy/core/ToolPolicyEngine.js.map +1 -1
  11. package/dist/policy/core/pnp-policy-inputs.d.ts +2 -3
  12. package/dist/policy/core/pnp-policy-inputs.js +2 -3
  13. package/dist/policy/core/pnp-policy-inputs.js.map +1 -1
  14. package/dist/policy/core/provenance.d.ts +7 -34
  15. package/dist/policy/core/provenance.js +4 -21
  16. package/dist/policy/core/provenance.js.map +1 -1
  17. package/dist/policy/sources/PnpPolicySource.d.ts +6 -19
  18. package/dist/policy/sources/PnpPolicySource.js +6 -19
  19. package/dist/policy/sources/PnpPolicySource.js.map +1 -1
  20. package/dist/runtime/SectionRuntimeEngine.d.ts +13 -28
  21. package/dist/runtime/SectionRuntimeEngine.js +13 -27
  22. package/dist/runtime/SectionRuntimeEngine.js.map +1 -1
  23. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +2 -2
  24. package/dist/runtime/adapter/SectionEngineAdapter.js +2 -2
  25. package/dist/runtime/adapter/SectionEngineAdapter.js.map +1 -1
  26. package/dist/runtime/adapter/coordinator-bridge.d.ts +1 -1
  27. package/dist/runtime/adapter/coordinator-bridge.js +1 -1
  28. package/dist/runtime/adapter/coordinator-bridge.js.map +1 -1
  29. package/dist/runtime/adapter/dom-event-bridge.d.ts +1 -1
  30. package/dist/runtime/adapter/dom-event-bridge.js +1 -1
  31. package/dist/runtime/adapter/dom-event-bridge.js.map +1 -1
  32. package/dist/runtime/catalog-registration.d.ts +12 -0
  33. package/dist/runtime/catalog-registration.js +54 -0
  34. package/dist/runtime/catalog-registration.js.map +1 -0
  35. package/dist/runtime/core/engine-output.d.ts +2 -2
  36. package/dist/runtime/core/engine-output.js +2 -2
  37. package/dist/runtime/core/engine-output.js.map +1 -1
  38. package/dist/runtime/core/engine-readiness.d.ts +2 -2
  39. package/dist/runtime/core/engine-readiness.js.map +1 -1
  40. package/dist/runtime/core/engine-resolver.d.ts +13 -41
  41. package/dist/runtime/core/engine-resolver.js +19 -76
  42. package/dist/runtime/core/engine-resolver.js.map +1 -1
  43. package/dist/runtime/core/engine-transition.d.ts +1 -1
  44. package/dist/runtime/core/engine-transition.js +1 -1
  45. package/dist/runtime/core/engine-transition.js.map +1 -1
  46. package/dist/runtime/engine.d.ts +1 -1
  47. package/dist/runtime/engine.js.map +1 -1
  48. package/dist/runtime/section-runtime-engine-host-context.d.ts +22 -15
  49. package/dist/runtime/section-runtime-engine-host-context.js +10 -10
  50. package/dist/runtime/section-runtime-engine-host-context.js.map +1 -1
  51. package/dist/runtime/stage-emit-gate.d.ts +4 -4
  52. package/dist/runtime/stage-emit-gate.js +3 -3
  53. package/dist/runtime/stage-emit-gate.js.map +1 -1
  54. package/dist/services/AccessibilityCatalogResolver.d.ts +23 -0
  55. package/dist/services/AccessibilityCatalogResolver.js +200 -28
  56. package/dist/services/AccessibilityCatalogResolver.js.map +1 -1
  57. package/dist/services/HighlightCoordinator.d.ts +25 -0
  58. package/dist/services/HighlightCoordinator.js +125 -0
  59. package/dist/services/HighlightCoordinator.js.map +1 -1
  60. package/dist/services/SSMLExtractor.d.ts +16 -1
  61. package/dist/services/SSMLExtractor.js +86 -7
  62. package/dist/services/SSMLExtractor.js.map +1 -1
  63. package/dist/services/TTSService.d.ts +30 -4
  64. package/dist/services/TTSService.js +652 -159
  65. package/dist/services/TTSService.js.map +1 -1
  66. package/dist/services/ToolRegistry.d.ts +2 -2
  67. package/dist/services/ToolRegistry.js +1 -1
  68. package/dist/services/ToolRegistry.js.map +1 -1
  69. package/dist/services/ToolkitCoordinator.d.ts +16 -73
  70. package/dist/services/ToolkitCoordinator.js +34 -113
  71. package/dist/services/ToolkitCoordinator.js.map +1 -1
  72. package/dist/services/interfaces.d.ts +20 -2
  73. package/dist/services/interfaces.js.map +1 -1
  74. package/dist/services/tool-config-validation.d.ts +1 -1
  75. package/dist/services/tool-config-validation.js +7 -7
  76. package/dist/services/tool-config-validation.js.map +1 -1
  77. package/dist/services/tool-instance-id.js +1 -1
  78. package/dist/services/tool-instance-id.js.map +1 -1
  79. package/dist/services/tool-providers/TTSToolProvider.js +3 -3
  80. package/dist/services/tool-providers/TTSToolProvider.js.map +1 -1
  81. package/dist/services/tools-config-normalizer.d.ts +13 -2
  82. package/dist/services/tools-config-normalizer.js +3 -8
  83. package/dist/services/tools-config-normalizer.js.map +1 -1
  84. package/dist/services/tts/browser-provider.js +2 -0
  85. package/dist/services/tts/browser-provider.js.map +1 -1
  86. package/dist/services/tts/catalog-span-alignment.d.ts +39 -0
  87. package/dist/services/tts/catalog-span-alignment.js +300 -0
  88. package/dist/services/tts/catalog-span-alignment.js.map +1 -0
  89. package/dist/services/tts/generated-speech/assemble-plan.d.ts +33 -0
  90. package/dist/services/tts/generated-speech/assemble-plan.js +111 -0
  91. package/dist/services/tts/generated-speech/assemble-plan.js.map +1 -0
  92. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +25 -0
  93. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +66 -0
  94. package/dist/services/tts/generated-speech/dom/build-dom-plan.js.map +1 -0
  95. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +19 -0
  96. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.js +71 -0
  97. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.js.map +1 -0
  98. package/dist/services/tts/generated-speech/dom/types.d.ts +37 -0
  99. package/dist/services/tts/generated-speech/dom/types.js +2 -0
  100. package/dist/services/tts/generated-speech/dom/types.js.map +1 -0
  101. package/dist/services/tts/generated-speech/index.d.ts +17 -0
  102. package/dist/services/tts/generated-speech/index.js +17 -0
  103. package/dist/services/tts/generated-speech/index.js.map +1 -0
  104. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +15 -0
  105. package/dist/services/tts/generated-speech/math-speech-cache.js +78 -0
  106. package/dist/services/tts/generated-speech/math-speech-cache.js.map +1 -0
  107. package/dist/services/tts/generated-speech/types.d.ts +104 -0
  108. package/dist/services/tts/generated-speech/types.js +18 -0
  109. package/dist/services/tts/generated-speech/types.js.map +1 -0
  110. package/dist/services/tts/highlight-pipeline/boundary-normalizer.d.ts +2 -0
  111. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +106 -0
  112. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js.map +1 -0
  113. package/dist/services/tts/highlight-pipeline/create-highlight-plan.d.ts +6 -0
  114. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +217 -0
  115. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js.map +1 -0
  116. package/dist/services/tts/highlight-pipeline/highlight-policy.d.ts +2 -0
  117. package/dist/services/tts/highlight-pipeline/highlight-policy.js +32 -0
  118. package/dist/services/tts/highlight-pipeline/highlight-policy.js.map +1 -0
  119. package/dist/services/tts/highlight-pipeline/index.d.ts +9 -0
  120. package/dist/services/tts/highlight-pipeline/index.js +9 -0
  121. package/dist/services/tts/highlight-pipeline/index.js.map +1 -0
  122. package/dist/services/tts/highlight-pipeline/math-semantic-resolver.d.ts +3 -0
  123. package/dist/services/tts/highlight-pipeline/math-semantic-resolver.js +32 -0
  124. package/dist/services/tts/highlight-pipeline/math-semantic-resolver.js.map +1 -0
  125. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.d.ts +29 -0
  126. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +44 -0
  127. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js.map +1 -0
  128. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.d.ts +2 -0
  129. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +20 -0
  130. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js.map +1 -0
  131. package/dist/services/tts/highlight-pipeline/readable-region.d.ts +1 -0
  132. package/dist/services/tts/highlight-pipeline/readable-region.js +19 -0
  133. package/dist/services/tts/highlight-pipeline/readable-region.js.map +1 -0
  134. package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.d.ts +6 -0
  135. package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.js +198 -0
  136. package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.js.map +1 -0
  137. package/dist/services/tts/highlight-pipeline/types.d.ts +72 -0
  138. package/dist/services/tts/highlight-pipeline/types.js +2 -0
  139. package/dist/services/tts/highlight-pipeline/types.js.map +1 -0
  140. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +2 -0
  141. package/dist/services/tts/highlight-pipeline/visible-map-range.js +22 -0
  142. package/dist/services/tts/highlight-pipeline/visible-map-range.js.map +1 -0
  143. package/dist/services/tts/math-alignment/index.d.ts +30 -0
  144. package/dist/services/tts/math-alignment/index.js +66 -0
  145. package/dist/services/tts/math-alignment/index.js.map +1 -0
  146. package/dist/services/tts/math-alignment/mathml-tokenizer.d.ts +2 -0
  147. package/dist/services/tts/math-alignment/mathml-tokenizer.js +317 -0
  148. package/dist/services/tts/math-alignment/mathml-tokenizer.js.map +1 -0
  149. package/dist/services/tts/math-alignment/range-resolver.d.ts +10 -0
  150. package/dist/services/tts/math-alignment/range-resolver.js +13 -0
  151. package/dist/services/tts/math-alignment/range-resolver.js.map +1 -0
  152. package/dist/services/tts/math-alignment/sequence-aligner.d.ts +7 -0
  153. package/dist/services/tts/math-alignment/sequence-aligner.js +119 -0
  154. package/dist/services/tts/math-alignment/sequence-aligner.js.map +1 -0
  155. package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +31 -0
  156. package/dist/services/tts/math-alignment/speech-tokenizer.js +143 -0
  157. package/dist/services/tts/math-alignment/speech-tokenizer.js.map +1 -0
  158. package/dist/services/tts/math-alignment/types.d.ts +58 -0
  159. package/dist/services/tts/math-alignment/types.js +2 -0
  160. package/dist/services/tts/math-alignment/types.js.map +1 -0
  161. package/dist/services/tts/math-aware-text-processing.d.ts +18 -0
  162. package/dist/services/tts/math-aware-text-processing.js +348 -0
  163. package/dist/services/tts/math-aware-text-processing.js.map +1 -0
  164. package/dist/services/tts/math-speech.d.ts +44 -0
  165. package/dist/services/tts/math-speech.js +147 -0
  166. package/dist/services/tts/math-speech.js.map +1 -0
  167. package/dist/services/tts/mathml-sanitization.d.ts +1 -0
  168. package/dist/services/tts/mathml-sanitization.js +170 -0
  169. package/dist/services/tts/mathml-sanitization.js.map +1 -0
  170. package/dist/services/tts/ssml/spoken-text.d.ts +33 -0
  171. package/dist/services/tts/ssml/spoken-text.js +159 -0
  172. package/dist/services/tts/ssml/spoken-text.js.map +1 -0
  173. package/dist/services/tts/text-processing.d.ts +3 -0
  174. package/dist/services/tts/text-processing.js +30 -4
  175. package/dist/services/tts/text-processing.js.map +1 -1
  176. package/dist/services/tts-runtime-config.d.ts +17 -1
  177. package/dist/services/tts-runtime-config.js +11 -1
  178. package/dist/services/tts-runtime-config.js.map +1 -1
  179. package/dist/tools/registrations/accessibility-tools.js +0 -1
  180. package/dist/tools/registrations/accessibility-tools.js.map +1 -1
  181. package/dist/tools/registrations/calculator.js +41 -2
  182. package/dist/tools/registrations/calculator.js.map +1 -1
  183. package/dist/tools/tool-tag-map.js +0 -1
  184. package/dist/tools/tool-tag-map.js.map +1 -1
  185. package/package.json +8 -11
  186. package/dist/services/PNPToolResolver.d.ts +0 -280
  187. package/dist/services/PNPToolResolver.js +0 -514
  188. package/dist/services/PNPToolResolver.js.map +0 -1
  189. package/dist/services/pnp-provenance.d.ts +0 -166
  190. package/dist/services/pnp-provenance.js +0 -231
  191. package/dist/services/pnp-provenance.js.map +0 -1
@@ -0,0 +1,317 @@
1
+ const OPERATOR_ALIASES = new Map([
2
+ ["+", ["plus"]],
3
+ ["-", ["minus", "negative"]],
4
+ ["−", ["minus", "negative"]],
5
+ ["×", ["times", "multiplied by"]],
6
+ ["÷", ["divided by"]],
7
+ ["=", ["equals", "equal to"]],
8
+ ["≠", ["is not equal to", "not equal to"]],
9
+ ["≈", ["approximately equals"]],
10
+ ["≤", ["less than or equal to"]],
11
+ ["≥", ["greater than or equal to"]],
12
+ ["<", ["less than"]],
13
+ [">", ["greater than"]],
14
+ ["∠", ["angle"]],
15
+ ["△", ["triangle"]],
16
+ ["□", ["box"]],
17
+ ["○", ["circle"]],
18
+ ["°", ["degrees", "degree"]],
19
+ ["%", ["percent"]],
20
+ ["'", ["prime"]],
21
+ ["′", ["prime"]],
22
+ ["•", ["times"]],
23
+ ["·", ["times"]],
24
+ ["\u2062", ["times", "multiplied by"]],
25
+ ]);
26
+ // Invisible operators (no glyph). A TTS engine usually does not voice them, so
27
+ // they are tokenized as OPTIONAL — they only consume a spoken token when one
28
+ // happens to match (e.g. an invisible-times read aloud as "times"). Marking
29
+ // them required would force a coarse fallback for any function call (sin, f(x))
30
+ // or implicit product.
31
+ const INVISIBLE_OPERATOR_ROLES = new Map([
32
+ ["\u2061", "function-application"],
33
+ ["\u2062", "implicit-multiply"],
34
+ ["\u2063", "invisible-separator"],
35
+ ["\u2064", "invisible-plus"],
36
+ ]);
37
+ // Exponents a TTS engine reliably reads as a single power word. Other exponents
38
+ // (variables, 4 and up, nested) are voiced in ways we cannot anchor, so they
39
+ // recurse and let the all-or-nothing cover check fall back to the expression.
40
+ const SUPERSCRIPT_ALIASES = new Map([
41
+ ["2", ["squared"]],
42
+ ["3", ["cubed"]],
43
+ ]);
44
+ // 2D / scripted containers we do not attempt to track per token. Each becomes a
45
+ // single opaque, unmatchable structure token so any expression containing one
46
+ // degrades to a coarse whole-expression highlight rather than risking a
47
+ // misaligned per-glyph highlight from flattened inner tokens.
48
+ const OPAQUE_STRUCTURE_ROLES = new Map([
49
+ ["msqrt", "radical"],
50
+ ["mroot", "radical"],
51
+ ["msub", "subscript"],
52
+ ["msubsup", "subsupscript"],
53
+ ["munder", "underscript"],
54
+ ["mover", "overscript"],
55
+ ["munderover", "underoverscript"],
56
+ ["mmultiscripts", "multiscript"],
57
+ ["menclose", "enclosure"],
58
+ ]);
59
+ const normalizeWhitespace = (value) => value.replace(/\s+/g, " ").trim();
60
+ const localNameOf = (element) => (element.localName || element.tagName || "").toLowerCase();
61
+ const isElement = (node) => node.nodeType === Node.ELEMENT_NODE;
62
+ const elementChildren = (element) => Array.from(element.childNodes).filter(isElement);
63
+ const textContentOf = (element) => normalizeWhitespace(element.textContent || "");
64
+ const elementTarget = (element, quality) => ({
65
+ type: "element-range",
66
+ quality,
67
+ element,
68
+ });
69
+ const tokenKindFor = (tagName) => {
70
+ switch (tagName) {
71
+ case "mi":
72
+ return "identifier";
73
+ case "mn":
74
+ return "number";
75
+ case "mo":
76
+ return "operator";
77
+ case "mtext":
78
+ case "ms":
79
+ return "text";
80
+ case "mspace":
81
+ return "space";
82
+ default:
83
+ return "unknown";
84
+ }
85
+ };
86
+ const normalizeToken = (text, kind) => {
87
+ if (kind === "operator") {
88
+ if (text === "−")
89
+ return "-";
90
+ return text;
91
+ }
92
+ return text.toLowerCase();
93
+ };
94
+ const aliasesFor = (text, kind, role) => {
95
+ if (role === "fraction")
96
+ return ["over", "divided by"];
97
+ if (role === "superscript")
98
+ return SUPERSCRIPT_ALIASES.get(text) ?? [];
99
+ if (text === "|")
100
+ return ["absolute value"];
101
+ if (kind === "operator")
102
+ return OPERATOR_ALIASES.get(text) || [];
103
+ return [];
104
+ };
105
+ const createToken = (args) => {
106
+ const normalized = args.normalized ?? normalizeToken(args.text, args.kind || "unknown");
107
+ return {
108
+ id: `math-token-${args.state.nextId++}`,
109
+ kind: args.kind,
110
+ role: args.role,
111
+ text: args.text,
112
+ normalized,
113
+ spokenAliases: aliasesFor(args.text, args.kind, args.role),
114
+ sourceElement: args.sourceElement,
115
+ path: args.path,
116
+ target: args.target || elementTarget(args.sourceElement, "element-range"),
117
+ optional: args.optional,
118
+ };
119
+ };
120
+ const pushToken = (state, token, collection = "tokens") => {
121
+ state[collection].push(token);
122
+ };
123
+ const visitChildren = (element, state, path) => {
124
+ elementChildren(element).forEach((child, index) => visitElement(child, state, `${path}/${localNameOf(child)}[${index}]`));
125
+ };
126
+ const visitFraction = (element, state, path) => {
127
+ const children = elementChildren(element);
128
+ if (children[0])
129
+ visitElement(children[0], state, `${path}/numerator`);
130
+ pushToken(state, createToken({
131
+ state,
132
+ kind: "structure",
133
+ role: "fraction",
134
+ text: "/",
135
+ sourceElement: element,
136
+ path,
137
+ target: elementTarget(element, "element-range"),
138
+ }));
139
+ if (children[1])
140
+ visitElement(children[1], state, `${path}/denominator`);
141
+ };
142
+ const visitFenced = (element, state, path) => {
143
+ const open = element.getAttribute("open") || "(";
144
+ const close = element.getAttribute("close") || ")";
145
+ pushToken(state, createToken({
146
+ state,
147
+ kind: "operator",
148
+ role: open === "|" ? "absolute-value-fence" : "open-fence",
149
+ text: open,
150
+ sourceElement: element,
151
+ path: `${path}/open`,
152
+ target: elementTarget(element, "element-range"),
153
+ }));
154
+ visitChildren(element, state, path);
155
+ pushToken(state, createToken({
156
+ state,
157
+ kind: "operator",
158
+ role: close === "|" ? "absolute-value-fence" : "close-fence",
159
+ text: close,
160
+ sourceElement: element,
161
+ path: `${path}/close`,
162
+ target: elementTarget(element, "element-range"),
163
+ }));
164
+ };
165
+ const visitSuperscript = (element, state, path) => {
166
+ const children = elementChildren(element);
167
+ if (children[0])
168
+ visitElement(children[0], state, `${path}/base`);
169
+ const exponent = children[1];
170
+ if (!exponent)
171
+ return;
172
+ const exponentText = textContentOf(exponent);
173
+ if (SUPERSCRIPT_ALIASES.has(exponentText)) {
174
+ // One token aliased to the power word ("squared" / "cubed"), targeting the
175
+ // whole superscript so the spoken power word highlights the base-and-
176
+ // exponent unit.
177
+ pushToken(state, createToken({
178
+ state,
179
+ kind: "structure",
180
+ role: "superscript",
181
+ text: exponentText,
182
+ normalized: exponentText,
183
+ sourceElement: exponent,
184
+ path: `${path}/exponent`,
185
+ target: elementTarget(element, "element-range"),
186
+ }));
187
+ return;
188
+ }
189
+ visitElement(exponent, state, `${path}/exponent`);
190
+ };
191
+ const visitOpaqueStructure = (element, state, path, role) => {
192
+ // A single required, unmatchable token. The NUL-prefixed normalized value can
193
+ // never equal a spoken token, so the cover check always falls back to the
194
+ // whole-expression target when this structure is present — no per-glyph
195
+ // guessing inside fractions-under-radicals, subscripts, accents, etc.
196
+ pushToken(state, createToken({
197
+ state,
198
+ kind: "structure",
199
+ role,
200
+ text: textContentOf(element) || role,
201
+ normalized: `\u0000${role}`,
202
+ sourceElement: element,
203
+ path,
204
+ target: elementTarget(element, "element-range"),
205
+ }));
206
+ };
207
+ const visitTable = (element, state, path) => {
208
+ let rowNumber = 0;
209
+ for (const child of elementChildren(element)) {
210
+ if (localNameOf(child) === "mtr" || localNameOf(child) === "mlabeledtr") {
211
+ rowNumber++;
212
+ pushToken(state, createToken({
213
+ state,
214
+ kind: "layout",
215
+ role: "table-row",
216
+ text: `row-${rowNumber}`,
217
+ normalized: `row-${rowNumber}`,
218
+ sourceElement: child,
219
+ path: `${path}/row[${rowNumber}]`,
220
+ target: elementTarget(child, "region-fallback"),
221
+ }), "layoutTargets");
222
+ }
223
+ visitElement(child, state, `${path}/${localNameOf(child)}[${rowNumber}]`);
224
+ }
225
+ };
226
+ const visitTokenElement = (element, state, path) => {
227
+ const text = textContentOf(element);
228
+ if (!text)
229
+ return;
230
+ const kind = tokenKindFor(localNameOf(element));
231
+ const invisibleRole = kind === "operator" ? INVISIBLE_OPERATOR_ROLES.get(text) : undefined;
232
+ const role = invisibleRole ??
233
+ (kind === "operator" && (text === "(" || text === ")")
234
+ ? "grouping-fence"
235
+ : kind);
236
+ pushToken(state, createToken({
237
+ state,
238
+ kind,
239
+ role,
240
+ text,
241
+ sourceElement: element,
242
+ path,
243
+ target: invisibleRole && element.parentElement
244
+ ? elementTarget(element.parentElement, "region-fallback")
245
+ : undefined,
246
+ optional: Boolean(invisibleRole) || role === "grouping-fence",
247
+ }));
248
+ };
249
+ const visitElement = (element, state, path) => {
250
+ const tagName = localNameOf(element);
251
+ switch (tagName) {
252
+ case "annotation":
253
+ case "annotation-xml":
254
+ return;
255
+ case "semantics": {
256
+ const presentation = elementChildren(element).find((child) => {
257
+ const childName = localNameOf(child);
258
+ return childName !== "annotation" && childName !== "annotation-xml";
259
+ });
260
+ if (presentation)
261
+ visitElement(presentation, state, `${path}/presentation`);
262
+ return;
263
+ }
264
+ case "mi":
265
+ case "mn":
266
+ case "mo":
267
+ case "mtext":
268
+ case "ms":
269
+ case "mspace":
270
+ visitTokenElement(element, state, path);
271
+ return;
272
+ case "mfrac":
273
+ visitFraction(element, state, path);
274
+ return;
275
+ case "msup":
276
+ visitSuperscript(element, state, path);
277
+ return;
278
+ case "mfenced":
279
+ visitFenced(element, state, path);
280
+ return;
281
+ case "mtable":
282
+ visitTable(element, state, path);
283
+ return;
284
+ case "mphantom":
285
+ // Invisible spacing content — must not become spoken tokens.
286
+ return;
287
+ case "maction": {
288
+ // Only the shown alternative (first child) is rendered/spoken.
289
+ const shown = elementChildren(element)[0];
290
+ if (shown)
291
+ visitElement(shown, state, `${path}/action`);
292
+ return;
293
+ }
294
+ default: {
295
+ const opaqueRole = OPAQUE_STRUCTURE_ROLES.get(tagName);
296
+ if (opaqueRole) {
297
+ visitOpaqueStructure(element, state, path, opaqueRole);
298
+ return;
299
+ }
300
+ visitChildren(element, state, path);
301
+ }
302
+ }
303
+ };
304
+ export const tokenizeMathML = (mathElement) => {
305
+ const state = {
306
+ tokens: [],
307
+ layoutTargets: [],
308
+ nextId: 0,
309
+ };
310
+ visitElement(mathElement, state, "/math");
311
+ return {
312
+ tokens: state.tokens,
313
+ layoutTargets: state.layoutTargets,
314
+ expressionTarget: elementTarget(mathElement, "region-fallback"),
315
+ };
316
+ };
317
+ //# sourceMappingURL=mathml-tokenizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mathml-tokenizer.js","sourceRoot":"","sources":["../../../../src/services/tts/math-alignment/mathml-tokenizer.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tHighlightTarget,\n\tMathAlignmentToken,\n\tMathAlignmentTokenKind,\n\tMathMLTokenizationResult,\n} from \"./types.js\";\n\nconst OPERATOR_ALIASES = new Map<string, string[]>([\n\t[\"+\", [\"plus\"]],\n\t[\"-\", [\"minus\", \"negative\"]],\n\t[\"−\", [\"minus\", \"negative\"]],\n\t[\"×\", [\"times\", \"multiplied by\"]],\n\t[\"÷\", [\"divided by\"]],\n\t[\"=\", [\"equals\", \"equal to\"]],\n\t[\"≠\", [\"is not equal to\", \"not equal to\"]],\n\t[\"≈\", [\"approximately equals\"]],\n\t[\"≤\", [\"less than or equal to\"]],\n\t[\"≥\", [\"greater than or equal to\"]],\n\t[\"<\", [\"less than\"]],\n\t[\">\", [\"greater than\"]],\n\t[\"∠\", [\"angle\"]],\n\t[\"△\", [\"triangle\"]],\n\t[\"□\", [\"box\"]],\n\t[\"○\", [\"circle\"]],\n\t[\"°\", [\"degrees\", \"degree\"]],\n\t[\"%\", [\"percent\"]],\n\t[\"'\", [\"prime\"]],\n\t[\"′\", [\"prime\"]],\n\t[\"•\", [\"times\"]],\n\t[\"·\", [\"times\"]],\n\t[\"\\u2062\", [\"times\", \"multiplied by\"]],\n]);\n\n// Invisible operators (no glyph). A TTS engine usually does not voice them, so\n// they are tokenized as OPTIONAL — they only consume a spoken token when one\n// happens to match (e.g. an invisible-times read aloud as \"times\"). Marking\n// them required would force a coarse fallback for any function call (sin, f(x))\n// or implicit product.\nconst INVISIBLE_OPERATOR_ROLES = new Map<string, string>([\n\t[\"\\u2061\", \"function-application\"],\n\t[\"\\u2062\", \"implicit-multiply\"],\n\t[\"\\u2063\", \"invisible-separator\"],\n\t[\"\\u2064\", \"invisible-plus\"],\n]);\n\n// Exponents a TTS engine reliably reads as a single power word. Other exponents\n// (variables, 4 and up, nested) are voiced in ways we cannot anchor, so they\n// recurse and let the all-or-nothing cover check fall back to the expression.\nconst SUPERSCRIPT_ALIASES = new Map<string, string[]>([\n\t[\"2\", [\"squared\"]],\n\t[\"3\", [\"cubed\"]],\n]);\n\n// 2D / scripted containers we do not attempt to track per token. Each becomes a\n// single opaque, unmatchable structure token so any expression containing one\n// degrades to a coarse whole-expression highlight rather than risking a\n// misaligned per-glyph highlight from flattened inner tokens.\nconst OPAQUE_STRUCTURE_ROLES = new Map<string, string>([\n\t[\"msqrt\", \"radical\"],\n\t[\"mroot\", \"radical\"],\n\t[\"msub\", \"subscript\"],\n\t[\"msubsup\", \"subsupscript\"],\n\t[\"munder\", \"underscript\"],\n\t[\"mover\", \"overscript\"],\n\t[\"munderover\", \"underoverscript\"],\n\t[\"mmultiscripts\", \"multiscript\"],\n\t[\"menclose\", \"enclosure\"],\n]);\n\nconst normalizeWhitespace = (value: string): string =>\n\tvalue.replace(/\\s+/g, \" \").trim();\n\nconst localNameOf = (element: Element): string =>\n\t(element.localName || element.tagName || \"\").toLowerCase();\n\nconst isElement = (node: Node): node is Element =>\n\tnode.nodeType === Node.ELEMENT_NODE;\n\nconst elementChildren = (element: Element): Element[] =>\n\tArray.from(element.childNodes).filter(isElement);\n\nconst textContentOf = (element: Element): string =>\n\tnormalizeWhitespace(element.textContent || \"\");\n\nconst elementTarget = (\n\telement: Element,\n\tquality: \"element-range\" | \"region-fallback\",\n): HighlightTarget => ({\n\ttype: \"element-range\",\n\tquality,\n\telement,\n});\n\nconst tokenKindFor = (tagName: string): MathAlignmentTokenKind => {\n\tswitch (tagName) {\n\t\tcase \"mi\":\n\t\t\treturn \"identifier\";\n\t\tcase \"mn\":\n\t\t\treturn \"number\";\n\t\tcase \"mo\":\n\t\t\treturn \"operator\";\n\t\tcase \"mtext\":\n\t\tcase \"ms\":\n\t\t\treturn \"text\";\n\t\tcase \"mspace\":\n\t\t\treturn \"space\";\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n};\n\nconst normalizeToken = (text: string, kind: MathAlignmentTokenKind): string => {\n\tif (kind === \"operator\") {\n\t\tif (text === \"−\") return \"-\";\n\t\treturn text;\n\t}\n\treturn text.toLowerCase();\n};\n\nconst aliasesFor = (\n\ttext: string,\n\tkind: MathAlignmentTokenKind,\n\trole: string,\n): string[] => {\n\tif (role === \"fraction\") return [\"over\", \"divided by\"];\n\tif (role === \"superscript\") return SUPERSCRIPT_ALIASES.get(text) ?? [];\n\tif (text === \"|\") return [\"absolute value\"];\n\tif (kind === \"operator\") return OPERATOR_ALIASES.get(text) || [];\n\treturn [];\n};\n\ninterface TokenizerState {\n\ttokens: MathAlignmentToken[];\n\tlayoutTargets: MathAlignmentToken[];\n\tnextId: number;\n}\n\nconst createToken = (args: {\n\tstate: TokenizerState;\n\tkind: MathAlignmentTokenKind;\n\trole: string;\n\ttext: string;\n\tnormalized?: string;\n\tsourceElement: Element;\n\tpath: string;\n\ttarget?: HighlightTarget;\n\toptional?: boolean;\n}): MathAlignmentToken => {\n\tconst normalized =\n\t\targs.normalized ?? normalizeToken(args.text, args.kind || \"unknown\");\n\treturn {\n\t\tid: `math-token-${args.state.nextId++}`,\n\t\tkind: args.kind,\n\t\trole: args.role,\n\t\ttext: args.text,\n\t\tnormalized,\n\t\tspokenAliases: aliasesFor(args.text, args.kind, args.role),\n\t\tsourceElement: args.sourceElement,\n\t\tpath: args.path,\n\t\ttarget: args.target || elementTarget(args.sourceElement, \"element-range\"),\n\t\toptional: args.optional,\n\t};\n};\n\nconst pushToken = (\n\tstate: TokenizerState,\n\ttoken: MathAlignmentToken,\n\tcollection: \"tokens\" | \"layoutTargets\" = \"tokens\",\n): void => {\n\tstate[collection].push(token);\n};\n\nconst visitChildren = (\n\telement: Element,\n\tstate: TokenizerState,\n\tpath: string,\n): void => {\n\telementChildren(element).forEach((child, index) =>\n\t\tvisitElement(child, state, `${path}/${localNameOf(child)}[${index}]`),\n\t);\n};\n\nconst visitFraction = (\n\telement: Element,\n\tstate: TokenizerState,\n\tpath: string,\n): void => {\n\tconst children = elementChildren(element);\n\tif (children[0]) visitElement(children[0], state, `${path}/numerator`);\n\tpushToken(\n\t\tstate,\n\t\tcreateToken({\n\t\t\tstate,\n\t\t\tkind: \"structure\",\n\t\t\trole: \"fraction\",\n\t\t\ttext: \"/\",\n\t\t\tsourceElement: element,\n\t\t\tpath,\n\t\t\ttarget: elementTarget(element, \"element-range\"),\n\t\t}),\n\t);\n\tif (children[1]) visitElement(children[1], state, `${path}/denominator`);\n};\n\nconst visitFenced = (\n\telement: Element,\n\tstate: TokenizerState,\n\tpath: string,\n): void => {\n\tconst open = element.getAttribute(\"open\") || \"(\";\n\tconst close = element.getAttribute(\"close\") || \")\";\n\tpushToken(\n\t\tstate,\n\t\tcreateToken({\n\t\t\tstate,\n\t\t\tkind: \"operator\",\n\t\t\trole: open === \"|\" ? \"absolute-value-fence\" : \"open-fence\",\n\t\t\ttext: open,\n\t\t\tsourceElement: element,\n\t\t\tpath: `${path}/open`,\n\t\t\ttarget: elementTarget(element, \"element-range\"),\n\t\t}),\n\t);\n\tvisitChildren(element, state, path);\n\tpushToken(\n\t\tstate,\n\t\tcreateToken({\n\t\t\tstate,\n\t\t\tkind: \"operator\",\n\t\t\trole: close === \"|\" ? \"absolute-value-fence\" : \"close-fence\",\n\t\t\ttext: close,\n\t\t\tsourceElement: element,\n\t\t\tpath: `${path}/close`,\n\t\t\ttarget: elementTarget(element, \"element-range\"),\n\t\t}),\n\t);\n};\n\nconst visitSuperscript = (\n\telement: Element,\n\tstate: TokenizerState,\n\tpath: string,\n): void => {\n\tconst children = elementChildren(element);\n\tif (children[0]) visitElement(children[0], state, `${path}/base`);\n\tconst exponent = children[1];\n\tif (!exponent) return;\n\tconst exponentText = textContentOf(exponent);\n\tif (SUPERSCRIPT_ALIASES.has(exponentText)) {\n\t\t// One token aliased to the power word (\"squared\" / \"cubed\"), targeting the\n\t\t// whole superscript so the spoken power word highlights the base-and-\n\t\t// exponent unit.\n\t\tpushToken(\n\t\t\tstate,\n\t\t\tcreateToken({\n\t\t\t\tstate,\n\t\t\t\tkind: \"structure\",\n\t\t\t\trole: \"superscript\",\n\t\t\t\ttext: exponentText,\n\t\t\t\tnormalized: exponentText,\n\t\t\t\tsourceElement: exponent,\n\t\t\t\tpath: `${path}/exponent`,\n\t\t\t\ttarget: elementTarget(element, \"element-range\"),\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\tvisitElement(exponent, state, `${path}/exponent`);\n};\n\nconst visitOpaqueStructure = (\n\telement: Element,\n\tstate: TokenizerState,\n\tpath: string,\n\trole: string,\n): void => {\n\t// A single required, unmatchable token. The NUL-prefixed normalized value can\n\t// never equal a spoken token, so the cover check always falls back to the\n\t// whole-expression target when this structure is present — no per-glyph\n\t// guessing inside fractions-under-radicals, subscripts, accents, etc.\n\tpushToken(\n\t\tstate,\n\t\tcreateToken({\n\t\t\tstate,\n\t\t\tkind: \"structure\",\n\t\t\trole,\n\t\t\ttext: textContentOf(element) || role,\n\t\t\tnormalized: `\\u0000${role}`,\n\t\t\tsourceElement: element,\n\t\t\tpath,\n\t\t\ttarget: elementTarget(element, \"element-range\"),\n\t\t}),\n\t);\n};\n\nconst visitTable = (\n\telement: Element,\n\tstate: TokenizerState,\n\tpath: string,\n): void => {\n\tlet rowNumber = 0;\n\tfor (const child of elementChildren(element)) {\n\t\tif (localNameOf(child) === \"mtr\" || localNameOf(child) === \"mlabeledtr\") {\n\t\t\trowNumber++;\n\t\t\tpushToken(\n\t\t\t\tstate,\n\t\t\t\tcreateToken({\n\t\t\t\t\tstate,\n\t\t\t\t\tkind: \"layout\",\n\t\t\t\t\trole: \"table-row\",\n\t\t\t\t\ttext: `row-${rowNumber}`,\n\t\t\t\t\tnormalized: `row-${rowNumber}`,\n\t\t\t\t\tsourceElement: child,\n\t\t\t\t\tpath: `${path}/row[${rowNumber}]`,\n\t\t\t\t\ttarget: elementTarget(child, \"region-fallback\"),\n\t\t\t\t}),\n\t\t\t\t\"layoutTargets\",\n\t\t\t);\n\t\t}\n\t\tvisitElement(child, state, `${path}/${localNameOf(child)}[${rowNumber}]`);\n\t}\n};\n\nconst visitTokenElement = (\n\telement: Element,\n\tstate: TokenizerState,\n\tpath: string,\n): void => {\n\tconst text = textContentOf(element);\n\tif (!text) return;\n\tconst kind = tokenKindFor(localNameOf(element));\n\tconst invisibleRole =\n\t\tkind === \"operator\" ? INVISIBLE_OPERATOR_ROLES.get(text) : undefined;\n\tconst role =\n\t\tinvisibleRole ??\n\t\t(kind === \"operator\" && (text === \"(\" || text === \")\")\n\t\t\t? \"grouping-fence\"\n\t\t\t: kind);\n\tpushToken(\n\t\tstate,\n\t\tcreateToken({\n\t\t\tstate,\n\t\t\tkind,\n\t\t\trole,\n\t\t\ttext,\n\t\t\tsourceElement: element,\n\t\t\tpath,\n\t\t\ttarget:\n\t\t\t\tinvisibleRole && element.parentElement\n\t\t\t\t\t? elementTarget(element.parentElement, \"region-fallback\")\n\t\t\t\t\t: undefined,\n\t\t\toptional: Boolean(invisibleRole) || role === \"grouping-fence\",\n\t\t}),\n\t);\n};\n\nconst visitElement = (\n\telement: Element,\n\tstate: TokenizerState,\n\tpath: string,\n): void => {\n\tconst tagName = localNameOf(element);\n\tswitch (tagName) {\n\t\tcase \"annotation\":\n\t\tcase \"annotation-xml\":\n\t\t\treturn;\n\t\tcase \"semantics\": {\n\t\t\tconst presentation = elementChildren(element).find((child) => {\n\t\t\t\tconst childName = localNameOf(child);\n\t\t\t\treturn childName !== \"annotation\" && childName !== \"annotation-xml\";\n\t\t\t});\n\t\t\tif (presentation) visitElement(presentation, state, `${path}/presentation`);\n\t\t\treturn;\n\t\t}\n\t\tcase \"mi\":\n\t\tcase \"mn\":\n\t\tcase \"mo\":\n\t\tcase \"mtext\":\n\t\tcase \"ms\":\n\t\tcase \"mspace\":\n\t\t\tvisitTokenElement(element, state, path);\n\t\t\treturn;\n\t\tcase \"mfrac\":\n\t\t\tvisitFraction(element, state, path);\n\t\t\treturn;\n\t\tcase \"msup\":\n\t\t\tvisitSuperscript(element, state, path);\n\t\t\treturn;\n\t\tcase \"mfenced\":\n\t\t\tvisitFenced(element, state, path);\n\t\t\treturn;\n\t\tcase \"mtable\":\n\t\t\tvisitTable(element, state, path);\n\t\t\treturn;\n\t\tcase \"mphantom\":\n\t\t\t// Invisible spacing content — must not become spoken tokens.\n\t\t\treturn;\n\t\tcase \"maction\": {\n\t\t\t// Only the shown alternative (first child) is rendered/spoken.\n\t\t\tconst shown = elementChildren(element)[0];\n\t\t\tif (shown) visitElement(shown, state, `${path}/action`);\n\t\t\treturn;\n\t\t}\n\t\tdefault: {\n\t\t\tconst opaqueRole = OPAQUE_STRUCTURE_ROLES.get(tagName);\n\t\t\tif (opaqueRole) {\n\t\t\t\tvisitOpaqueStructure(element, state, path, opaqueRole);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvisitChildren(element, state, path);\n\t\t}\n\t}\n};\n\nexport const tokenizeMathML = (mathElement: Element): MathMLTokenizationResult => {\n\tconst state: TokenizerState = {\n\t\ttokens: [],\n\t\tlayoutTargets: [],\n\t\tnextId: 0,\n\t};\n\tvisitElement(mathElement, state, \"/math\");\n\n\treturn {\n\t\ttokens: state.tokens,\n\t\tlayoutTargets: state.layoutTargets,\n\t\texpressionTarget: elementTarget(mathElement, \"region-fallback\"),\n\t};\n};\n"]}
@@ -0,0 +1,10 @@
1
+ import type { AlignmentResult, HighlightTarget } from "./types.js";
2
+ import type { ResolvedSpeechBoundary } from "./speech-tokenizer.js";
3
+ export declare const resolveHighlightTargetForBoundary: (args: {
4
+ alignment: AlignmentResult;
5
+ boundary: ResolvedSpeechBoundary | null;
6
+ }) => HighlightTarget;
7
+ export declare const resolveMatchedHighlightTargetForBoundary: (args: {
8
+ alignment: AlignmentResult;
9
+ boundary: ResolvedSpeechBoundary | null;
10
+ }) => HighlightTarget | null;
@@ -0,0 +1,13 @@
1
+ export const resolveHighlightTargetForBoundary = (args) => {
2
+ if (!args.boundary)
3
+ return args.alignment.fallbackTarget;
4
+ const segment = args.alignment.segments.find((candidate) => candidate.speechTokenIds.includes(args.boundary.token.id));
5
+ return segment?.target || args.alignment.fallbackTarget;
6
+ };
7
+ export const resolveMatchedHighlightTargetForBoundary = (args) => {
8
+ if (!args.boundary)
9
+ return null;
10
+ const segment = args.alignment.segments.find((candidate) => candidate.speechTokenIds.includes(args.boundary.token.id));
11
+ return segment?.target || null;
12
+ };
13
+ //# sourceMappingURL=range-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"range-resolver.js","sourceRoot":"","sources":["../../../../src/services/tts/math-alignment/range-resolver.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC,IAGjD,EAAmB,EAAE;IACrB,IAAI,CAAC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;IACzD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAC1D,SAAS,CAAC,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAC1D,CAAC;IACF,OAAO,OAAO,EAAE,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;AACzD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,wCAAwC,GAAG,CAAC,IAGxD,EAA0B,EAAE;IAC5B,IAAI,CAAC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAC1D,SAAS,CAAC,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAC1D,CAAC;IACF,OAAO,OAAO,EAAE,MAAM,IAAI,IAAI,CAAC;AAChC,CAAC,CAAC","sourcesContent":["import type { AlignmentResult, HighlightTarget } from \"./types.js\";\nimport type { ResolvedSpeechBoundary } from \"./speech-tokenizer.js\";\n\nexport const resolveHighlightTargetForBoundary = (args: {\n\talignment: AlignmentResult;\n\tboundary: ResolvedSpeechBoundary | null;\n}): HighlightTarget => {\n\tif (!args.boundary) return args.alignment.fallbackTarget;\n\tconst segment = args.alignment.segments.find((candidate) =>\n\t\tcandidate.speechTokenIds.includes(args.boundary!.token.id),\n\t);\n\treturn segment?.target || args.alignment.fallbackTarget;\n};\n\nexport const resolveMatchedHighlightTargetForBoundary = (args: {\n\talignment: AlignmentResult;\n\tboundary: ResolvedSpeechBoundary | null;\n}): HighlightTarget | null => {\n\tif (!args.boundary) return null;\n\tconst segment = args.alignment.segments.find((candidate) =>\n\t\tcandidate.speechTokenIds.includes(args.boundary!.token.id),\n\t);\n\treturn segment?.target || null;\n};\n"]}
@@ -0,0 +1,7 @@
1
+ import type { AlignmentResult, MathMLTokenizationResult } from "./types.js";
2
+ import type { SpeechSourceTokenization } from "./speech-tokenizer.js";
3
+ export declare const alignSpeechToMath: (args: {
4
+ speech: SpeechSourceTokenization;
5
+ math: MathMLTokenizationResult;
6
+ minExactWordConfidence?: number;
7
+ }) => AlignmentResult;
@@ -0,0 +1,119 @@
1
+ const MIN_EXACT_WORD_CONFIDENCE = 0.95;
2
+ // Candidate spoken forms for a math token: its own normalized value plus every
3
+ // spoken alias, each split into the sequence of words a TTS engine emits (e.g.
4
+ // "÷" → ["divided", "by"], "≤" → ["less", "than", "or", "equal", "to"]). This is
5
+ // what lets multi-word operator names match — a single speech token can never
6
+ // equal "divided by". Speech tokens are already number-word normalized by the
7
+ // speech tokenizer (two → 2), so no numeric mapping is repeated here.
8
+ const candidatePhrasesFor = (math) => {
9
+ const phrases = [];
10
+ const seen = new Set();
11
+ const add = (value) => {
12
+ const words = value.toLowerCase().trim().split(/\s+/).filter(Boolean);
13
+ if (words.length === 0)
14
+ return;
15
+ const key = words.join(" ");
16
+ if (seen.has(key))
17
+ return;
18
+ seen.add(key);
19
+ phrases.push(words);
20
+ };
21
+ add(math.normalized);
22
+ for (const alias of math.spokenAliases)
23
+ add(alias);
24
+ return phrases;
25
+ };
26
+ // Length (in speech tokens) of the longest candidate phrase that matches the
27
+ // run of speech tokens starting at `startIndex`, or 0 if none match. Longest
28
+ // match wins so a multi-word alias is preferred over a coincidental one-word
29
+ // prefix.
30
+ const matchPhraseLengthAt = (speechTokens, startIndex, phrases) => {
31
+ let best = 0;
32
+ for (const phrase of phrases) {
33
+ if (phrase.length <= best)
34
+ continue;
35
+ let matched = true;
36
+ for (let i = 0; i < phrase.length; i++) {
37
+ const token = speechTokens[startIndex + i];
38
+ if (!token || token.normalized.toLowerCase() !== phrase[i]) {
39
+ matched = false;
40
+ break;
41
+ }
42
+ }
43
+ if (matched)
44
+ best = phrase.length;
45
+ }
46
+ return best;
47
+ };
48
+ // Greedy forward (monotonic) pass: each math token consumes the next run of
49
+ // speech tokens that spells one of its candidate phrases. Optional math tokens
50
+ // (invisible operators, grouping fences) are consumed only when present.
51
+ // Unmatched speech tokens are skipped, which the perfect-cover check below then
52
+ // treats as a miss.
53
+ const computeMonotonicMatches = (speechTokens, mathTokens) => {
54
+ const matches = [];
55
+ let speechIndex = 0;
56
+ for (const math of mathTokens) {
57
+ const phrases = candidatePhrasesFor(math);
58
+ if (math.optional) {
59
+ const consumed = matchPhraseLengthAt(speechTokens, speechIndex, phrases);
60
+ if (consumed > 0) {
61
+ matches.push({
62
+ speechTokens: speechTokens.slice(speechIndex, speechIndex + consumed),
63
+ math,
64
+ });
65
+ speechIndex += consumed;
66
+ }
67
+ continue;
68
+ }
69
+ while (speechIndex < speechTokens.length) {
70
+ const consumed = matchPhraseLengthAt(speechTokens, speechIndex, phrases);
71
+ if (consumed > 0) {
72
+ matches.push({
73
+ speechTokens: speechTokens.slice(speechIndex, speechIndex + consumed),
74
+ math,
75
+ });
76
+ speechIndex += consumed;
77
+ break;
78
+ }
79
+ speechIndex++;
80
+ }
81
+ }
82
+ return matches;
83
+ };
84
+ // Word-level highlighting is all-or-nothing: it is emitted only when every
85
+ // required math token AND every spoken token is accounted for. Anything less
86
+ // falls back to a coarse whole-expression highlight, because "false positives
87
+ // are worse than coarse highlighting" — a partial alignment risks underlining
88
+ // the wrong glyph.
89
+ const isPerfectCover = (matches, speechTokens, mathTokens) => {
90
+ const requiredTokenCount = mathTokens.filter((token) => !token.optional).length;
91
+ const requiredMatched = matches.filter((match) => !match.math.optional).length;
92
+ const consumedSpeech = matches.reduce((total, match) => total + match.speechTokens.length, 0);
93
+ return (requiredTokenCount > 0 &&
94
+ requiredMatched === requiredTokenCount &&
95
+ consumedSpeech === speechTokens.length);
96
+ };
97
+ const createSegments = (matches) => matches.map(({ speechTokens, math }) => ({
98
+ // Every spoken word that spells this math token maps back to it, so a
99
+ // boundary on any of those words ("divided" or "by") resolves to the
100
+ // same glyph.
101
+ speechTokenIds: speechTokens.map((token) => token.id),
102
+ mathTokenIds: [math.id],
103
+ score: 1,
104
+ confidence: 1,
105
+ target: math.target,
106
+ }));
107
+ export const alignSpeechToMath = (args) => {
108
+ const minConfidence = args.minExactWordConfidence ?? MIN_EXACT_WORD_CONFIDENCE;
109
+ const matches = computeMonotonicMatches(args.speech.tokens, args.math.tokens);
110
+ const confidence = isPerfectCover(matches, args.speech.tokens, args.math.tokens)
111
+ ? 1
112
+ : 0;
113
+ return {
114
+ segments: confidence >= minConfidence ? createSegments(matches) : [],
115
+ confidence,
116
+ fallbackTarget: args.math.expressionTarget,
117
+ };
118
+ };
119
+ //# sourceMappingURL=sequence-aligner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequence-aligner.js","sourceRoot":"","sources":["../../../../src/services/tts/math-alignment/sequence-aligner.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tAlignmentResult,\n\tAlignmentSegment,\n\tMathAlignmentToken,\n\tMathMLTokenizationResult,\n\tSpeechAlignmentToken,\n} from \"./types.js\";\nimport type { SpeechSourceTokenization } from \"./speech-tokenizer.js\";\n\nconst MIN_EXACT_WORD_CONFIDENCE = 0.95;\n\ninterface PhraseMatch {\n\tspeechTokens: SpeechAlignmentToken[];\n\tmath: MathAlignmentToken;\n}\n\n// Candidate spoken forms for a math token: its own normalized value plus every\n// spoken alias, each split into the sequence of words a TTS engine emits (e.g.\n// \"÷\" → [\"divided\", \"by\"], \"≤\" → [\"less\", \"than\", \"or\", \"equal\", \"to\"]). This is\n// what lets multi-word operator names match — a single speech token can never\n// equal \"divided by\". Speech tokens are already number-word normalized by the\n// speech tokenizer (two → 2), so no numeric mapping is repeated here.\nconst candidatePhrasesFor = (math: MathAlignmentToken): string[][] => {\n\tconst phrases: string[][] = [];\n\tconst seen = new Set<string>();\n\tconst add = (value: string): void => {\n\t\tconst words = value.toLowerCase().trim().split(/\\s+/).filter(Boolean);\n\t\tif (words.length === 0) return;\n\t\tconst key = words.join(\" \");\n\t\tif (seen.has(key)) return;\n\t\tseen.add(key);\n\t\tphrases.push(words);\n\t};\n\tadd(math.normalized);\n\tfor (const alias of math.spokenAliases) add(alias);\n\treturn phrases;\n};\n\n// Length (in speech tokens) of the longest candidate phrase that matches the\n// run of speech tokens starting at `startIndex`, or 0 if none match. Longest\n// match wins so a multi-word alias is preferred over a coincidental one-word\n// prefix.\nconst matchPhraseLengthAt = (\n\tspeechTokens: SpeechAlignmentToken[],\n\tstartIndex: number,\n\tphrases: string[][],\n): number => {\n\tlet best = 0;\n\tfor (const phrase of phrases) {\n\t\tif (phrase.length <= best) continue;\n\t\tlet matched = true;\n\t\tfor (let i = 0; i < phrase.length; i++) {\n\t\t\tconst token = speechTokens[startIndex + i];\n\t\t\tif (!token || token.normalized.toLowerCase() !== phrase[i]) {\n\t\t\t\tmatched = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (matched) best = phrase.length;\n\t}\n\treturn best;\n};\n\n// Greedy forward (monotonic) pass: each math token consumes the next run of\n// speech tokens that spells one of its candidate phrases. Optional math tokens\n// (invisible operators, grouping fences) are consumed only when present.\n// Unmatched speech tokens are skipped, which the perfect-cover check below then\n// treats as a miss.\nconst computeMonotonicMatches = (\n\tspeechTokens: SpeechAlignmentToken[],\n\tmathTokens: MathAlignmentToken[],\n): PhraseMatch[] => {\n\tconst matches: PhraseMatch[] = [];\n\tlet speechIndex = 0;\n\n\tfor (const math of mathTokens) {\n\t\tconst phrases = candidatePhrasesFor(math);\n\t\tif (math.optional) {\n\t\t\tconst consumed = matchPhraseLengthAt(speechTokens, speechIndex, phrases);\n\t\t\tif (consumed > 0) {\n\t\t\t\tmatches.push({\n\t\t\t\t\tspeechTokens: speechTokens.slice(\n\t\t\t\t\t\tspeechIndex,\n\t\t\t\t\t\tspeechIndex + consumed,\n\t\t\t\t\t),\n\t\t\t\t\tmath,\n\t\t\t\t});\n\t\t\t\tspeechIndex += consumed;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\twhile (speechIndex < speechTokens.length) {\n\t\t\tconst consumed = matchPhraseLengthAt(speechTokens, speechIndex, phrases);\n\t\t\tif (consumed > 0) {\n\t\t\t\tmatches.push({\n\t\t\t\t\tspeechTokens: speechTokens.slice(\n\t\t\t\t\t\tspeechIndex,\n\t\t\t\t\t\tspeechIndex + consumed,\n\t\t\t\t\t),\n\t\t\t\t\tmath,\n\t\t\t\t});\n\t\t\t\tspeechIndex += consumed;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tspeechIndex++;\n\t\t}\n\t}\n\n\treturn matches;\n};\n\n// Word-level highlighting is all-or-nothing: it is emitted only when every\n// required math token AND every spoken token is accounted for. Anything less\n// falls back to a coarse whole-expression highlight, because \"false positives\n// are worse than coarse highlighting\" — a partial alignment risks underlining\n// the wrong glyph.\nconst isPerfectCover = (\n\tmatches: PhraseMatch[],\n\tspeechTokens: SpeechAlignmentToken[],\n\tmathTokens: MathAlignmentToken[],\n): boolean => {\n\tconst requiredTokenCount = mathTokens.filter(\n\t\t(token) => !token.optional,\n\t).length;\n\tconst requiredMatched = matches.filter(\n\t\t(match) => !match.math.optional,\n\t).length;\n\tconst consumedSpeech = matches.reduce(\n\t\t(total, match) => total + match.speechTokens.length,\n\t\t0,\n\t);\n\treturn (\n\t\trequiredTokenCount > 0 &&\n\t\trequiredMatched === requiredTokenCount &&\n\t\tconsumedSpeech === speechTokens.length\n\t);\n};\n\nconst createSegments = (matches: PhraseMatch[]): AlignmentSegment[] =>\n\tmatches.map(({ speechTokens, math }) => ({\n\t\t// Every spoken word that spells this math token maps back to it, so a\n\t\t// boundary on any of those words (\"divided\" or \"by\") resolves to the\n\t\t// same glyph.\n\t\tspeechTokenIds: speechTokens.map((token) => token.id),\n\t\tmathTokenIds: [math.id],\n\t\tscore: 1,\n\t\tconfidence: 1,\n\t\ttarget: math.target,\n\t}));\n\nexport const alignSpeechToMath = (args: {\n\tspeech: SpeechSourceTokenization;\n\tmath: MathMLTokenizationResult;\n\tminExactWordConfidence?: number;\n}): AlignmentResult => {\n\tconst minConfidence =\n\t\targs.minExactWordConfidence ?? MIN_EXACT_WORD_CONFIDENCE;\n\tconst matches = computeMonotonicMatches(args.speech.tokens, args.math.tokens);\n\tconst confidence = isPerfectCover(\n\t\tmatches,\n\t\targs.speech.tokens,\n\t\targs.math.tokens,\n\t)\n\t\t? 1\n\t\t: 0;\n\n\treturn {\n\t\tsegments: confidence >= minConfidence ? createSegments(matches) : [],\n\t\tconfidence,\n\t\tfallbackTarget: args.math.expressionTarget,\n\t};\n};\n"]}
@@ -0,0 +1,31 @@
1
+ import type { BoundaryOffsetSpace, SpeechAlignmentToken } from "./types.js";
2
+ export interface SpeechSourceTokenization {
3
+ speechText: string;
4
+ spokenText: string;
5
+ tokens: SpeechAlignmentToken[];
6
+ rawToSpokenOffsetMap: Map<number, number>;
7
+ boundaryOffsetSpace: BoundaryOffsetSpace;
8
+ unsupportedSemantic: boolean;
9
+ hasMarkup: boolean;
10
+ }
11
+ export interface ResolvedSpeechBoundary {
12
+ token: SpeechAlignmentToken;
13
+ start: number;
14
+ length: number;
15
+ confidence: number;
16
+ }
17
+ export declare const tokenizeSpeechSource: (args: {
18
+ speechText: string;
19
+ }) => SpeechSourceTokenization;
20
+ export declare const resolveBoundaryToSpeechToken: (args: {
21
+ tokenization: SpeechSourceTokenization;
22
+ position: number;
23
+ length?: number;
24
+ boundaryWord?: string;
25
+ }) => ResolvedSpeechBoundary | null;
26
+ export declare const resolveSpokenOffsetToSpeechToken: (args: {
27
+ tokenization: SpeechSourceTokenization;
28
+ position: number;
29
+ length?: number;
30
+ boundaryWord?: string;
31
+ }) => ResolvedSpeechBoundary | null;