@pie-players/pie-assessment-toolkit 0.3.74 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +658 -714
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +1 -21
  4. package/dist/attempt/TestSession.js +16 -102
  5. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  6. package/dist/components/ItemScope.custom-element.js +1 -0
  7. package/dist/components/ItemToolBar.custom-element.js +1 -1
  8. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  9. package/dist/components/SectionToolBar.custom-element.js +1 -1
  10. package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
  11. package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
  13. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  14. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
  15. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  16. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  17. package/dist/components/item-scope-element.d.ts +2 -0
  18. package/dist/components/item-scope-element.js +2 -0
  19. package/dist/context/assessment-toolkit-context.d.ts +33 -6
  20. package/dist/index.d.ts +25 -50
  21. package/dist/index.js +15 -45
  22. package/dist/policy/core/PolicySource.d.ts +2 -2
  23. package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
  24. package/dist/policy/core/ToolPolicyEngine.js +203 -56
  25. package/dist/policy/core/compose-decision.d.ts +28 -7
  26. package/dist/policy/core/compose-decision.js +95 -74
  27. package/dist/policy/core/decision-types.d.ts +98 -55
  28. package/dist/policy/core/feature-decision.d.ts +28 -19
  29. package/dist/policy/core/feature-decision.js +15 -11
  30. package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
  31. package/dist/policy/core/pnp-policy-inputs.js +40 -88
  32. package/dist/policy/core/provenance.d.ts +10 -5
  33. package/dist/policy/core/provenance.js +1 -1
  34. package/dist/policy/core/tool-parameters.d.ts +11 -0
  35. package/dist/policy/core/tool-parameters.js +15 -0
  36. package/dist/policy/engine.d.ts +7 -23
  37. package/dist/policy/engine.js +5 -20
  38. package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
  39. package/dist/policy/sources/PnpPolicySource.js +133 -132
  40. package/dist/runtime/SectionControllerBinding.d.ts +283 -0
  41. package/dist/runtime/SectionControllerBinding.js +456 -0
  42. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  43. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  44. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  45. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  46. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  47. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  48. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
  49. package/dist/runtime/adapter/subscriber-fanout.js +2 -6
  50. package/dist/runtime/catalog-context.d.ts +19 -0
  51. package/dist/runtime/catalog-context.js +28 -0
  52. package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
  53. package/dist/runtime/composition-emit-scheduler.js +10 -16
  54. package/dist/runtime/content-language.d.ts +32 -0
  55. package/dist/runtime/content-language.js +46 -0
  56. package/dist/runtime/content-region.d.ts +13 -0
  57. package/dist/runtime/content-region.js +29 -0
  58. package/dist/runtime/content-registration.d.ts +13 -0
  59. package/dist/runtime/content-registration.js +25 -0
  60. package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
  61. package/dist/runtime/core/SectionEngineCore.js +2 -6
  62. package/dist/runtime/core/cohort.d.ts +1 -6
  63. package/dist/runtime/core/cohort.js +1 -10
  64. package/dist/runtime/core/engine-input.d.ts +18 -30
  65. package/dist/runtime/core/engine-input.js +11 -12
  66. package/dist/runtime/core/engine-output.d.ts +17 -40
  67. package/dist/runtime/core/engine-output.js +9 -21
  68. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  69. package/dist/runtime/core/engine-readiness.js +8 -41
  70. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  71. package/dist/runtime/core/engine-resolver.js +18 -43
  72. package/dist/runtime/core/engine-state.d.ts +28 -36
  73. package/dist/runtime/core/engine-state.js +18 -21
  74. package/dist/runtime/core/engine-transition.d.ts +14 -19
  75. package/dist/runtime/core/engine-transition.js +74 -53
  76. package/dist/runtime/engine.d.ts +16 -19
  77. package/dist/runtime/engine.js +12 -19
  78. package/dist/runtime/page-warnings.d.ts +13 -0
  79. package/dist/runtime/page-warnings.js +22 -0
  80. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  81. package/dist/runtime/policy-input-forwarding.js +25 -0
  82. package/dist/runtime/registration-events.d.ts +22 -14
  83. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  84. package/dist/runtime/runtime-event-claim.js +15 -0
  85. package/dist/runtime/runtime-id.d.ts +1 -1
  86. package/dist/runtime/runtime-id.js +1 -1
  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 +194 -0
  93. package/dist/runtime/tool-host-contract.d.ts +8 -14
  94. package/dist/runtime/tool-host-contract.js +12 -12
  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/AccessibilityCatalogResolver.d.ts +21 -31
  98. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  99. package/dist/services/ElementToolStateStore.d.ts +42 -67
  100. package/dist/services/ElementToolStateStore.js +65 -71
  101. package/dist/services/HighlightCoordinator.d.ts +28 -40
  102. package/dist/services/HighlightCoordinator.js +459 -381
  103. package/dist/services/RangeSerializer.d.ts +11 -33
  104. package/dist/services/RangeSerializer.js +88 -59
  105. package/dist/services/SSMLExtractor.d.ts +8 -31
  106. package/dist/services/SSMLExtractor.js +18 -40
  107. package/dist/services/TTSService.d.ts +159 -119
  108. package/dist/services/TTSService.js +937 -822
  109. package/dist/services/ToolCoordinator.d.ts +42 -102
  110. package/dist/services/ToolCoordinator.js +178 -256
  111. package/dist/services/ToolRegistry.d.ts +152 -133
  112. package/dist/services/ToolRegistry.js +192 -178
  113. package/dist/services/ToolkitCoordinator.d.ts +233 -237
  114. package/dist/services/ToolkitCoordinator.js +615 -557
  115. package/dist/services/audio-handoff.d.ts +6 -7
  116. package/dist/services/audio-handoff.js +20 -14
  117. package/dist/services/catalog-owner.d.ts +0 -1
  118. package/dist/services/framework-error-bus.d.ts +3 -6
  119. package/dist/services/framework-error-bus.js +3 -6
  120. package/dist/services/framework-error.d.ts +51 -13
  121. package/dist/services/framework-error.js +19 -20
  122. package/dist/services/interfaces.d.ts +189 -184
  123. package/dist/services/section-controller-types.d.ts +7 -7
  124. package/dist/services/section-session-assignment.d.ts +21 -0
  125. package/dist/services/section-session-assignment.js +80 -0
  126. package/dist/services/spoken-audio-cards.d.ts +2 -6
  127. package/dist/services/spoken-audio-cards.js +2 -6
  128. package/dist/services/tool-config-defaults.d.ts +3 -3
  129. package/dist/services/tool-config-defaults.js +3 -3
  130. package/dist/services/tool-config-validation.d.ts +9 -12
  131. package/dist/services/tool-config-validation.js +47 -87
  132. package/dist/services/tool-context.d.ts +7 -31
  133. package/dist/services/tool-context.js +99 -65
  134. package/dist/services/tool-failure.d.ts +30 -0
  135. package/dist/services/tool-failure.js +11 -0
  136. package/dist/services/tool-icons.d.ts +5 -5
  137. package/dist/services/tool-icons.js +6 -5
  138. package/dist/services/tool-instance-id.d.ts +8 -7
  139. package/dist/services/tool-instance-id.js +12 -24
  140. package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
  141. package/dist/services/tool-providers/TTSToolProvider.js +32 -85
  142. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
  143. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
  144. package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
  145. package/dist/services/tool-request.d.ts +31 -12
  146. package/dist/services/tool-request.js +31 -16
  147. package/dist/services/toolbar-decided-tools.d.ts +16 -0
  148. package/dist/services/toolbar-decided-tools.js +29 -0
  149. package/dist/services/toolbar-items.js +2 -5
  150. package/dist/services/tools-config-normalizer.d.ts +33 -29
  151. package/dist/services/tools-config-normalizer.js +26 -28
  152. package/dist/services/tts/browser-provider.d.ts +39 -6
  153. package/dist/services/tts/browser-provider.js +280 -160
  154. package/dist/services/tts/catalog-span-alignment.js +14 -7
  155. package/dist/services/tts/flat-tree.d.ts +64 -0
  156. package/dist/services/tts/flat-tree.js +269 -0
  157. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  158. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  159. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  160. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  161. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  162. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  163. package/dist/services/tts/generated-speech/index.js +1 -1
  164. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  165. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  166. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  167. package/dist/services/tts/generated-speech/types.js +1 -1
  168. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
  169. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  170. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  171. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  172. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  173. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  174. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  175. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  176. package/dist/services/tts/math-aware-text-processing.js +42 -47
  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 +12 -1
  180. package/dist/services/tts/math-speech.js +54 -18
  181. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  182. package/dist/services/tts/mathml-sanitization.js +9 -1
  183. package/dist/services/tts/provider-options.d.ts +52 -0
  184. package/dist/services/tts/provider-options.js +1 -0
  185. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  186. package/dist/services/tts/ssml/spoken-text.js +29 -0
  187. package/dist/services/tts/start-failure.d.ts +4 -0
  188. package/dist/services/tts/start-failure.js +19 -0
  189. package/dist/services/tts/text-processing.d.ts +9 -0
  190. package/dist/services/tts/text-processing.js +46 -33
  191. package/dist/services/tts/text-segmentation.d.ts +12 -0
  192. package/dist/services/tts/text-segmentation.js +60 -10
  193. package/dist/services/tts-runtime-config.d.ts +54 -26
  194. package/dist/services/tts-runtime-config.js +27 -112
  195. package/dist/tools/content-capability-resolution.d.ts +9 -11
  196. package/dist/tools/content-capability-resolution.js +8 -45
  197. package/dist/tools/registration.d.ts +59 -0
  198. package/dist/tools/registration.js +55 -0
  199. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  200. package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
  201. package/dist/tools/tool-surface-host.d.ts +6 -10
  202. package/dist/tools/tool-surface-host.js +45 -50
  203. package/dist/tools/tool-tag-map.d.ts +5 -15
  204. package/dist/tools/tool-tag-map.js +6 -20
  205. package/dist/utils/structural-equality.d.ts +11 -0
  206. package/dist/utils/structural-equality.js +47 -0
  207. package/package.json +15 -29
  208. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  209. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  210. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  211. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  212. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  213. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  214. package/dist/context/runtime-context-consumer.d.ts +0 -13
  215. package/dist/context/runtime-context-consumer.js +0 -18
  216. package/dist/item-loader.d.ts +0 -44
  217. package/dist/item-loader.js +0 -61
  218. package/dist/policy/core/engine-context.d.ts +0 -29
  219. package/dist/policy/core/engine-context.js +0 -27
  220. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  221. package/dist/policy/core/policy-source-tag.js +0 -28
  222. package/dist/policy/internal.d.ts +0 -25
  223. package/dist/policy/internal.js +0 -25
  224. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  225. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  226. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  227. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  228. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  229. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  230. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  231. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  232. package/dist/runtime/engine-context.d.ts +0 -39
  233. package/dist/runtime/engine-context.js +0 -31
  234. package/dist/runtime/internal.d.ts +0 -43
  235. package/dist/runtime/internal.js +0 -45
  236. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  237. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  238. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  239. package/dist/runtime/stage-emit-gate.js +0 -82
  240. package/dist/services/ContextVariableStore.d.ts +0 -113
  241. package/dist/services/ContextVariableStore.js +0 -206
  242. package/dist/services/I18nService.d.ts +0 -47
  243. package/dist/services/I18nService.js +0 -84
  244. package/dist/services/ThemeProvider.d.ts +0 -95
  245. package/dist/services/ThemeProvider.js +0 -334
  246. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  247. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  248. package/dist/services/pnp-standard-features.d.ts +0 -238
  249. package/dist/services/pnp-standard-features.js +0 -338
  250. package/dist/services/tts-control-events.d.ts +0 -13
  251. package/dist/services/tts-control-events.js +0 -7
  252. package/dist/tools/client.d.ts +0 -10
  253. package/dist/tools/client.js +0 -10
  254. package/dist/tools/internal.d.ts +0 -44
  255. package/dist/tools/internal.js +0 -39
  256. package/dist/utils/logger.d.ts +0 -6
  257. package/dist/utils/logger.js +0 -10
@@ -1,19 +1,56 @@
1
+ import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
1
2
  import { setSreStartupLocaleSource, sreLocaleSource } from "./sre-locales.js";
2
3
  import { normalizeTextForSpeech } from "./text-processing.js";
3
- let sreLoadPromise = null;
4
+ // Debug lines need PIE_TTS_DEBUG=1 or globalThis.__PIE_TTS_DEBUG__ = true.
5
+ const logger = createPieLogger("tts-math-speech", isTtsDebugEnabled);
6
+ // SRE configures one global engine, so each setupEngine + toSpeech pair runs to
7
+ // completion before the next starts; concurrent reads would otherwise speak
8
+ // under each other's settings.
4
9
  let sreOperationQueue = Promise.resolve();
5
10
  const normalizeLocale = (language) => (language || "en").split("-")[0].toLowerCase() || "en";
6
- const defaultLoadSre = async (engineOptions) => {
7
- if (!sreLoadPromise) {
8
- setSreStartupLocaleSource(engineOptions);
9
- sreLoadPromise = import("./sre-engine.js").then(({ default: candidate }) => {
10
- if (!candidate || typeof candidate.toSpeech !== "function") {
11
- throw new Error("speech-rule-engine did not expose toSpeech");
12
- }
13
- return candidate;
14
- });
11
+ /**
12
+ * Loads the speech-rule-engine once per page. A failed load is dropped, so the
13
+ * next call tries again; meanwhile math reads as its visible text. The first
14
+ * failure warns, later ones log at debug.
15
+ */
16
+ export const createSreLoader = (importEngine) => {
17
+ let load = null;
18
+ let warned = false;
19
+ return (engineOptions) => {
20
+ if (!load) {
21
+ setSreStartupLocaleSource(engineOptions);
22
+ const attempt = importEngine();
23
+ load = attempt;
24
+ attempt.catch((error) => {
25
+ if (load === attempt)
26
+ load = null;
27
+ if (warned) {
28
+ logger.debug("speech-rule-engine failed to load again", error);
29
+ return;
30
+ }
31
+ warned = true;
32
+ logger.warn("speech-rule-engine failed to load; math reads as its visible text until a later read loads it.", error);
33
+ });
34
+ }
35
+ return load;
36
+ };
37
+ };
38
+ const defaultLoadSre = createSreLoader(() => import("./sre-engine.js").then(({ default: candidate }) => {
39
+ if (!candidate || typeof candidate.toSpeech !== "function") {
40
+ throw new Error("speech-rule-engine did not expose toSpeech");
15
41
  }
16
- return sreLoadPromise;
42
+ return candidate;
43
+ }));
44
+ /**
45
+ * One speech-rule-engine load for a whole read: its equations share the
46
+ * attempt, so a failed load costs a read one attempt.
47
+ */
48
+ export const createReadSreLoad = (mathSpeech) => {
49
+ let attempt = null;
50
+ return () => {
51
+ attempt ??= defaultLoadSre(normalizeSREMathSpeechOptions(mathSpeech)?.engineOptions);
52
+ return attempt;
53
+ };
17
54
  };
18
55
  // SRE's ClearSpeak rule set is English-only; MathSpeak is localized to many more
19
56
  // locales. Use ClearSpeak for English (highest-quality math prose) and MathSpeak
@@ -47,9 +84,9 @@ const setupSre = async (sre, language, markup, mathSpeech) => {
47
84
  ...engineOptions,
48
85
  locale,
49
86
  domain: normalizedMathSpeech?.domain || domainForLocale(locale),
50
- ...(normalizedMathSpeech?.style
51
- ? { style: normalizedMathSpeech.style }
52
- : {}),
87
+ // SRE keeps a style it was given until it is given another, so a call
88
+ // without one resets it: math control names set ClearSpeak preferences.
89
+ style: normalizedMathSpeech?.style || "default",
53
90
  modality: "speech",
54
91
  markup,
55
92
  });
@@ -80,7 +117,7 @@ const generateMathSsml = async (sre, mathml, language, mathSpeech) => {
80
117
  return ssml.includes("<speak") ? ssml : null;
81
118
  }
82
119
  catch (error) {
83
- console.debug("[TTSService] Math SSML generation failed; using plain", {
120
+ logger.debug("math SSML generation failed; using plain", {
84
121
  message: error instanceof Error ? error.message : String(error),
85
122
  });
86
123
  return null;
@@ -99,8 +136,7 @@ export const resolveMathSpeechFromChunks = async (chunks, options = {}) => {
99
136
  let sre = null;
100
137
  let usedMathSpeech = false;
101
138
  let usedFallback = false;
102
- const loadSre = options.loadSre ||
103
- (() => defaultLoadSre(normalizeSREMathSpeechOptions(options.mathSpeech)?.engineOptions));
139
+ const loadSre = options.loadSre || createReadSreLoad(options.mathSpeech);
104
140
  const speechParts = [];
105
141
  for (const chunk of chunks) {
106
142
  if (chunk.type === "text") {
@@ -122,7 +158,7 @@ export const resolveMathSpeechFromChunks = async (chunks, options = {}) => {
122
158
  }
123
159
  catch (error) {
124
160
  usedFallback = true;
125
- console.debug("[TTSService] Math speech generation failed; using visible fallback", {
161
+ logger.debug("math speech generation failed; using visible fallback", {
126
162
  message: error instanceof Error ? error.message : String(error),
127
163
  });
128
164
  speechParts.push(chunk.fallbackText);
@@ -1 +1,8 @@
1
+ /**
2
+ * MathML re-serialized from an allow-list, with a MathML xmlns on the root.
3
+ * Null for empty or unparseable input, no DOMParser, a non-<math> root, a
4
+ * disallowed element or a node other than element/text/comment, or an xmlns
5
+ * other than MathML's. Comments, attributes off the allow-list and values
6
+ * containing javascript: or data:text/html are dropped.
7
+ */
1
8
  export declare const canonicalizeMathML: (input: string) => string | null;
@@ -1,4 +1,5 @@
1
1
  const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
2
+ const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
2
3
  const ALLOWED_MATHML_ELEMENTS = new Set([
3
4
  "math",
4
5
  "maction",
@@ -109,7 +110,7 @@ const isAllowedMathElement = (element) => {
109
110
  const namespace = element.namespaceURI;
110
111
  return (!namespace ||
111
112
  namespace === MATHML_NAMESPACE ||
112
- namespace === "http://www.w3.org/1999/xhtml");
113
+ namespace === XHTML_NAMESPACE);
113
114
  };
114
115
  const serializeMathNode = (node) => {
115
116
  if (node.nodeType === Node.TEXT_NODE) {
@@ -153,6 +154,13 @@ const parseMathML = (input) => {
153
154
  }
154
155
  return null;
155
156
  };
157
+ /**
158
+ * MathML re-serialized from an allow-list, with a MathML xmlns on the root.
159
+ * Null for empty or unparseable input, no DOMParser, a non-<math> root, a
160
+ * disallowed element or a node other than element/text/comment, or an xmlns
161
+ * other than MathML's. Comments, attributes off the allow-list and values
162
+ * containing javascript: or data:text/html are dropped.
163
+ */
156
164
  export const canonicalizeMathML = (input) => {
157
165
  const trimmed = String(input || "").trim();
158
166
  if (!trimmed)
@@ -0,0 +1,52 @@
1
+ import type { TTSConfig, TTSProviderOptions } from "@pie-players/pie-tts";
2
+ import type { SREMathSpeechOptions } from "./math-speech.js";
3
+ import type { BoundarySpacingMode } from "./text-processing.js";
4
+ /**
5
+ * The provider options the toolkit reads, on top of the ones every provider
6
+ * receives. Hosts configure them as free-form options, so readers still check
7
+ * each value at runtime.
8
+ */
9
+ export interface ToolkitTTSProviderOptions extends TTSProviderOptions {
10
+ /**
11
+ * Highlight words or sentences while reading. Defaults by provider
12
+ * capability. A playback that sends no word boundaries highlights its
13
+ * sentence in either mode.
14
+ */
15
+ highlightMode?: "word" | "sentence";
16
+ /** Locale for text processing; the toolkit sets it per speak. */
17
+ locale?: string;
18
+ /** Speech-rule-engine options for math speech. */
19
+ mathSpeech?: SREMathSpeechOptions;
20
+ /** Sentence and word segmentation of the browser provider. */
21
+ segmenter?: {
22
+ /** `regexOnly` segments without `Intl.Segmenter`. */
23
+ mode?: "regexOnly";
24
+ locale?: string;
25
+ };
26
+ /** Pauses at structural boundaries: `baseMs` scaled by a strength unit, clamped. */
27
+ structuralPauses?: {
28
+ baseMs?: number | string;
29
+ units?: {
30
+ minor?: number | string;
31
+ section?: number | string;
32
+ major?: number | string;
33
+ };
34
+ minMs?: number | string;
35
+ maxMs?: number | string;
36
+ };
37
+ textNormalization?: {
38
+ boundarySpacingMode?: BoundarySpacingMode;
39
+ locale?: string;
40
+ };
41
+ }
42
+ /** A provider configuration as the toolkit reads it. */
43
+ export interface ToolkitTTSConfig extends TTSConfig {
44
+ providerOptions?: ToolkitTTSProviderOptions;
45
+ /**
46
+ * Highlight math token by token; `false` highlights each formula as one
47
+ * block. The highlight pipeline reads it, and providers ignore it.
48
+ *
49
+ * @default true
50
+ */
51
+ mathTokenHighlighting?: boolean;
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,29 +1,5 @@
1
- /**
2
- * Shared SSML → spoken-text extractor.
3
- *
4
- * Strips SSML markup to the words a TTS engine would actually voice, while
5
- * recording a `rawToSpokenOffsetMap` from raw-SSML character offsets to
6
- * normalized spoken-text offsets. This is the single source of truth for SSML
7
- * stripping used by:
8
- * - the catalog span aligner (`catalog-span-alignment.ts`),
9
- * - the math speech tokenizer (`math-alignment/speech-tokenizer.ts`),
10
- * - the runtime generated-speech path (PIE-623).
11
- *
12
- * Keeping one implementation guarantees provider word-boundary offsets (which
13
- * are into the raw SSML string) map to the same spoken text on every path.
14
- *
15
- * Tag handling:
16
- * - Structural tags (`speak`, `p`, `s`, `emphasis`, `prosody`, `say-as`,
17
- * `voice`, `lang`) are transparent; their text content flows through.
18
- * `p`/`s` open tags inject a word boundary space.
19
- * - `break` injects a space (a pause is a word boundary).
20
- * - `sub alias="…"` substitutes the alias text (and skips the original).
21
- * `sub` with no alias marks the content unsupported for word tracking.
22
- * - Semantic tags that change *what* is spoken without leaving trackable
23
- * text (`audio`, `phoneme`) mark the content unsupported.
24
- * - Any other unknown element marks the content unsupported (degrade to
25
- * coarse region highlighting rather than mis-track).
26
- */
1
+ /** Whether `text` is an SSML document: it opens with a `<speak>` element. */
2
+ export declare const isSsmlDocument: (text: string) => boolean;
27
3
  export interface ExtractedSpokenText {
28
4
  spokenText: string;
29
5
  rawToSpokenOffsetMap: Map<number, number>;
@@ -1,4 +1,33 @@
1
1
  import { normalizeTextForSpeech } from "../text-processing.js";
2
+ /**
3
+ * Shared SSML → spoken-text extractor.
4
+ *
5
+ * Strips SSML markup to the words a TTS engine would actually voice, while
6
+ * recording a `rawToSpokenOffsetMap` from raw-SSML character offsets to
7
+ * normalized spoken-text offsets. This is the single source of truth for SSML
8
+ * stripping used by:
9
+ * - the catalog span aligner (`catalog-span-alignment.ts`),
10
+ * - the math speech tokenizer (`math-alignment/speech-tokenizer.ts`),
11
+ * - the runtime generated-speech path.
12
+ *
13
+ * Keeping one implementation guarantees provider word-boundary offsets (which
14
+ * are into the raw SSML string) map to the same spoken text on every path.
15
+ *
16
+ * Tag handling:
17
+ * - Structural tags (`speak`, `p`, `s`, `emphasis`, `prosody`, `say-as`,
18
+ * `voice`, `lang`) are transparent; their text content flows through.
19
+ * `p`/`s` open tags inject a word boundary space.
20
+ * - `break` injects a space (a pause is a word boundary).
21
+ * - `sub alias="…"` substitutes the alias text (and skips the original).
22
+ * `sub` with no alias marks the content unsupported for word tracking.
23
+ * - Semantic tags that change *what* is spoken without leaving trackable
24
+ * text (`audio`, `phoneme`) mark the content unsupported.
25
+ * - Any other unknown element marks the content unsupported (degrade to
26
+ * coarse region highlighting rather than mis-track).
27
+ */
28
+ const SSML_DOCUMENT = /^\s*<speak[\s/>]/i;
29
+ /** Whether `text` is an SSML document: it opens with a `<speak>` element. */
30
+ export const isSsmlDocument = (text) => SSML_DOCUMENT.test(text);
2
31
  const STRUCTURAL_SSML_TAGS = new Set([
3
32
  "speak",
4
33
  "p",
@@ -0,0 +1,4 @@
1
+ /** Whether a speak rejected because speech could not start. */
2
+ export declare function isTTSStartFailure(error: unknown): boolean;
3
+ /** Marks `cause` as a start failure, keeping its message and stack. */
4
+ export declare function toTTSStartFailure(cause: unknown): Error;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The `code` a speak's rejection carries when speech could not start, as
3
+ * distinct from a failure while speaking. Read by shape: several copies of the
4
+ * toolkit can share a page, so an error class from one fails `instanceof` in
5
+ * another.
6
+ */
7
+ const TTS_START_FAILED_CODE = "tts-start-failed";
8
+ /** Whether a speak rejected because speech could not start. */
9
+ export function isTTSStartFailure(error) {
10
+ return (typeof error === "object" &&
11
+ error !== null &&
12
+ error.code === TTS_START_FAILED_CODE);
13
+ }
14
+ /** Marks `cause` as a start failure, keeping its message and stack. */
15
+ export function toTTSStartFailure(cause) {
16
+ const error = cause instanceof Error ? cause : new Error(String(cause));
17
+ error.code = TTS_START_FAILED_CODE;
18
+ return error;
19
+ }
@@ -62,6 +62,15 @@ export declare const collectRangeTextForSpeech: (range: Range, root: Element) =>
62
62
  text: string;
63
63
  filtered: boolean;
64
64
  };
65
+ /** The part of `textNode`'s text that `range` holds, for a node it intersects. */
66
+ export declare const textInRange: (textNode: Text, range: Range) => string;
67
+ /**
68
+ * Whether `range` holds all of `element`'s speakable text: its first and last
69
+ * speakable characters, which a contiguous range holding both holds everything
70
+ * between. An element with no speakable text, an image for one, is held when the
71
+ * range touches it, since no part of it can be selected alone.
72
+ */
73
+ export declare const rangeHoldsSpeakableElement: (range: Range, element: Element, root?: Element | null) => boolean;
65
74
  export declare const shouldInsertWordBoundarySpace: (previousChar: string | null, nextChar: string | null, options?: TextProcessingOptions) => boolean;
66
75
  export declare const collectVisibleTextAndMap: (element: Element, options?: TextProcessingOptions) => {
67
76
  text: string;
@@ -1,3 +1,4 @@
1
+ import { flatTreeParentElement, flatTreeTextNodes, rangeHoldsTextPosition, rangeIntersectsComposedNode, } from "./flat-tree.js";
1
2
  export const normalizeTextForSpeech = (text) => text.trim().replace(/\s+/g, " ");
2
3
  // Single source of truth for aligning spoken text to visible / math text.
3
4
  // Letters are matched with `\p{L}` so the tokenizer is Unicode-aware — accented
@@ -50,15 +51,15 @@ export const isElementHiddenForTTS = (element) => {
50
51
  return false;
51
52
  };
52
53
  export const isNodeHiddenForTTS = (node, root) => {
53
- let current = node.nodeType === 1
54
- ? node
55
- : node.parentElement;
54
+ // Up the flat tree, so a node in a shadow root inherits its host's
55
+ // visibility and a slotted node the visibility of the slot rendering it.
56
+ let current = node.nodeType === 1 ? node : flatTreeParentElement(node);
56
57
  while (current) {
57
58
  if (isElementHiddenForTTS(current))
58
59
  return true;
59
60
  if (root && current === root)
60
61
  break;
61
- current = current.parentElement;
62
+ current = flatTreeParentElement(current);
62
63
  }
63
64
  return false;
64
65
  };
@@ -112,15 +113,14 @@ export const isElementSuppressedForTTS = (element) => {
112
113
  return true;
113
114
  };
114
115
  export const isNodeSuppressedForTTS = (node, root) => {
115
- let current = node.nodeType === 1
116
- ? node
117
- : node.parentElement;
116
+ // Through shadow hosts as well: suppression on a host covers its shadow tree.
117
+ let current = node.nodeType === 1 ? node : flatTreeParentElement(node);
118
118
  while (current) {
119
119
  if (isElementSuppressedForTTS(current))
120
120
  return true;
121
121
  if (root && current === root)
122
122
  break;
123
- current = current.parentElement;
123
+ current = flatTreeParentElement(current);
124
124
  }
125
125
  return false;
126
126
  };
@@ -146,11 +146,7 @@ export const isNodeExcludedFromSpeech = (node, root) => isNodeHiddenForTTS(node,
146
146
  * nothing".
147
147
  */
148
148
  export const collectRangeTextForSpeech = (range, root) => {
149
- if (typeof document === "undefined" ||
150
- typeof document.createTreeWalker !==
151
- "function" ||
152
- typeof NodeFilter === "undefined" ||
153
- typeof range.intersectsNode !== "function") {
149
+ if (typeof range.intersectsNode !== "function") {
154
150
  // Degraded, and deliberately not silent about the difference: callers still
155
151
  // enforce whole-selection suppression from the range's common ancestor, so
156
152
  // the construct guard holds even here. What is lost is per-node filtering
@@ -159,25 +155,49 @@ export const collectRangeTextForSpeech = (range, root) => {
159
155
  }
160
156
  const parts = [];
161
157
  let filtered = false;
162
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
163
- let current = walker.nextNode();
164
- while (current) {
165
- const textNode = current;
166
- if (range.intersectsNode(textNode)) {
158
+ for (const textNode of flatTreeTextNodes(root)) {
159
+ if (rangeIntersectsComposedNode(range, textNode)) {
167
160
  if (isNodeExcludedFromSpeech(textNode, root)) {
168
161
  filtered = true;
169
162
  }
170
163
  else {
171
- const raw = textNode.textContent || "";
172
- const start = textNode === range.startContainer ? range.startOffset : 0;
173
- const end = textNode === range.endContainer ? range.endOffset : raw.length;
174
- parts.push(raw.slice(start, end));
164
+ parts.push(textInRange(textNode, range));
175
165
  }
176
166
  }
177
- current = walker.nextNode();
178
167
  }
179
168
  return { text: parts.join(""), filtered };
180
169
  };
170
+ /** The part of `textNode`'s text that `range` holds, for a node it intersects. */
171
+ export const textInRange = (textNode, range) => {
172
+ const raw = textNode.textContent || "";
173
+ const start = textNode === range.startContainer ? range.startOffset : 0;
174
+ const end = textNode === range.endContainer ? range.endOffset : raw.length;
175
+ return raw.slice(start, end);
176
+ };
177
+ /**
178
+ * Whether `range` holds all of `element`'s speakable text: its first and last
179
+ * speakable characters, which a contiguous range holding both holds everything
180
+ * between. An element with no speakable text, an image for one, is held when the
181
+ * range touches it, since no part of it can be selected alone.
182
+ */
183
+ export const rangeHoldsSpeakableElement = (range, element, root) => {
184
+ let first = null;
185
+ let last = null;
186
+ for (const textNode of flatTreeTextNodes(element)) {
187
+ if (isNodeExcludedFromSpeech(textNode, root))
188
+ continue;
189
+ const raw = textNode.textContent || "";
190
+ const firstOffset = raw.search(/\S/);
191
+ if (firstOffset === -1)
192
+ continue;
193
+ first ??= { node: textNode, offset: firstOffset };
194
+ last = { node: textNode, offset: raw.trimEnd().length - 1 };
195
+ }
196
+ if (!first || !last)
197
+ return rangeIntersectsComposedNode(range, element);
198
+ return (rangeHoldsTextPosition(range, first.node, first.offset) &&
199
+ rangeHoldsTextPosition(range, last.node, last.offset));
200
+ };
181
201
  export const shouldInsertWordBoundarySpace = (previousChar, nextChar, options) => {
182
202
  if (!previousChar || !nextChar)
183
203
  return false;
@@ -210,10 +230,7 @@ export const shouldInsertWordBoundarySpace = (previousChar, nextChar, options) =
210
230
  };
211
231
  export const collectVisibleTextAndMap = (element, options) => {
212
232
  const map = new Map();
213
- if (typeof document === "undefined" ||
214
- typeof document.createTreeWalker !==
215
- "function" ||
216
- typeof NodeFilter === "undefined") {
233
+ if (typeof document === "undefined") {
217
234
  return { text: "", map };
218
235
  }
219
236
  const outChars = [];
@@ -222,11 +239,8 @@ export const collectVisibleTextAndMap = (element, options) => {
222
239
  let lastCharWasWhitespace = false;
223
240
  let lastMapped = null;
224
241
  let previousVisibleChar = null;
225
- const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
226
- let current = walker.nextNode();
227
- while (current) {
228
- const textNode = current;
229
- const parent = textNode.parentElement;
242
+ for (const textNode of flatTreeTextNodes(element)) {
243
+ const parent = flatTreeParentElement(textNode);
230
244
  if (parent && !isNodeExcludedFromSpeech(textNode, element)) {
231
245
  const raw = textNode.textContent || "";
232
246
  const firstVisibleMatch = raw.match(/\S/);
@@ -274,7 +288,6 @@ export const collectVisibleTextAndMap = (element, options) => {
274
288
  }
275
289
  }
276
290
  }
277
- current = walker.nextNode();
278
291
  }
279
292
  const text = outChars.join("").trimEnd();
280
293
  while (map.has(text.length)) {
@@ -6,4 +6,16 @@ export interface SentenceSegmentationOptions {
6
6
  locale?: string;
7
7
  useSentenceSegmenter?: boolean;
8
8
  }
9
+ /**
10
+ * `text` split into sentences at their offsets in it, by Intl.Segmenter unless
11
+ * the policy disables it or it is missing. The segmenter path drops
12
+ * whitespace-only segments; the regex fallback keeps every match.
13
+ */
9
14
  export declare const segmentSentences: (text: string, options?: SentenceSegmentationOptions) => SentenceSegment[];
15
+ /**
16
+ * `text` in pieces of at most `maxLength` characters: whole sentences while they
17
+ * fit, a sentence too long for one piece cut at its last space that fits, and a
18
+ * word too long for one piece cut where the limit falls. Each piece is a trimmed
19
+ * substring of `text`, at its offset there.
20
+ */
21
+ export declare const splitTextToLength: (text: string, maxLength: number, options?: SentenceSegmentationOptions) => SentenceSegment[];
@@ -1,11 +1,14 @@
1
+ /**
2
+ * `text` split into sentences at their offsets in it, by Intl.Segmenter unless
3
+ * the policy disables it or it is missing. The segmenter path drops
4
+ * whitespace-only segments; the regex fallback keeps every match.
5
+ */
1
6
  export const segmentSentences = (text, options) => {
2
- const useSentenceSegmenter = options?.useSentenceSegmenter !== false;
3
- try {
4
- if (!useSentenceSegmenter) {
5
- throw new Error("Segmenter disabled by policy");
6
- }
7
- const Segmenter = globalThis.Intl?.Segmenter;
8
- if (typeof Segmenter === "function") {
7
+ const Segmenter = options?.useSentenceSegmenter === false
8
+ ? undefined
9
+ : globalThis.Intl?.Segmenter;
10
+ if (typeof Segmenter === "function") {
11
+ try {
9
12
  const segmenter = new Segmenter(options?.locale, {
10
13
  granularity: "sentence",
11
14
  });
@@ -18,9 +21,9 @@ export const segmentSentences = (text, options) => {
18
21
  if (parsed.length > 0)
19
22
  return parsed;
20
23
  }
21
- }
22
- catch {
23
- // Fall through to regex segmentation.
24
+ catch {
25
+ // Fall through to regex segmentation.
26
+ }
24
27
  }
25
28
  const sentenceRegex = /[^.!?]+(?:[.!?]+|$)/g;
26
29
  const raw = text.match(sentenceRegex) || [text];
@@ -35,3 +38,50 @@ export const segmentSentences = (text, options) => {
35
38
  }
36
39
  return parsed.length > 0 ? parsed : [{ text, offset: 0 }];
37
40
  };
41
+ /**
42
+ * `text` in pieces of at most `maxLength` characters: whole sentences while they
43
+ * fit, a sentence too long for one piece cut at its last space that fits, and a
44
+ * word too long for one piece cut where the limit falls. Each piece is a trimmed
45
+ * substring of `text`, at its offset there.
46
+ */
47
+ export const splitTextToLength = (text, maxLength, options) => {
48
+ const limit = Math.max(1, Math.floor(maxLength));
49
+ const pieces = [];
50
+ let start = -1;
51
+ let end = -1;
52
+ const flush = () => {
53
+ if (start >= 0)
54
+ pieces.push({ text: text.slice(start, end), offset: start });
55
+ start = -1;
56
+ };
57
+ const append = (spanStart, spanEnd) => {
58
+ if (start >= 0 && spanEnd - start <= limit) {
59
+ end = spanEnd;
60
+ return;
61
+ }
62
+ flush();
63
+ let cursor = spanStart;
64
+ while (spanEnd - cursor > limit) {
65
+ const window = text.slice(cursor, cursor + limit + 1);
66
+ const space = window.search(/\s\S*$/);
67
+ const cut = space > 0 ? cursor + space : cursor + limit;
68
+ pieces.push({ text: text.slice(cursor, cut).trimEnd(), offset: cursor });
69
+ cursor = cut;
70
+ while (cursor < spanEnd && /\s/.test(text[cursor]))
71
+ cursor++;
72
+ }
73
+ if (cursor < spanEnd) {
74
+ start = cursor;
75
+ end = spanEnd;
76
+ }
77
+ };
78
+ for (const sentence of segmentSentences(text, options)) {
79
+ const leading = sentence.text.search(/\S/);
80
+ if (leading === -1)
81
+ continue;
82
+ const spanStart = sentence.offset + leading;
83
+ append(spanStart, sentence.offset + sentence.text.trimEnd().length);
84
+ }
85
+ flush();
86
+ return pieces;
87
+ };