@pie-players/pie-assessment-toolkit 0.3.73 → 0.3.75

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