@pie-players/pie-assessment-toolkit 0.3.74 → 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 (228) hide show
  1. package/README.md +349 -313
  2. package/dist/attempt/TestSession.d.ts +1 -20
  3. package/dist/attempt/TestSession.js +8 -99
  4. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  5. package/dist/components/ItemScope.custom-element.js +1 -0
  6. package/dist/components/ItemToolBar.custom-element.js +1 -1
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  8. package/dist/components/SectionToolBar.custom-element.js +1 -1
  9. package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
  10. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-97hx60z4.js} +1 -1
  11. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-jx38w99s.js +2 -0
  13. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +40 -0
  14. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  15. package/dist/components/chunks/sre-engine-jd8cc08x.js +1 -0
  16. package/dist/components/item-scope-element.d.ts +2 -0
  17. package/dist/components/item-scope-element.js +2 -0
  18. package/dist/context/assessment-toolkit-context.d.ts +27 -6
  19. package/dist/index.d.ts +22 -47
  20. package/dist/index.js +12 -36
  21. package/dist/policy/core/PolicySource.d.ts +1 -1
  22. package/dist/policy/core/ToolPolicyEngine.d.ts +77 -32
  23. package/dist/policy/core/ToolPolicyEngine.js +136 -46
  24. package/dist/policy/core/compose-decision.d.ts +28 -7
  25. package/dist/policy/core/compose-decision.js +75 -54
  26. package/dist/policy/core/decision-types.d.ts +65 -34
  27. package/dist/policy/core/feature-decision.d.ts +16 -9
  28. package/dist/policy/core/feature-decision.js +14 -8
  29. package/dist/policy/core/pnp-policy-inputs.d.ts +24 -33
  30. package/dist/policy/core/pnp-policy-inputs.js +28 -56
  31. package/dist/policy/core/provenance.d.ts +9 -4
  32. package/dist/policy/core/provenance.js +1 -1
  33. package/dist/policy/engine.d.ts +7 -23
  34. package/dist/policy/engine.js +5 -20
  35. package/dist/policy/sources/PnpPolicySource.d.ts +44 -61
  36. package/dist/policy/sources/PnpPolicySource.js +117 -118
  37. package/dist/runtime/SectionControllerBinding.d.ts +279 -0
  38. package/dist/runtime/SectionControllerBinding.js +450 -0
  39. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  40. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  41. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  42. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  43. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  44. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  45. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -2
  46. package/dist/runtime/adapter/subscriber-fanout.js +1 -2
  47. package/dist/runtime/catalog-context.d.ts +19 -0
  48. package/dist/runtime/catalog-context.js +28 -0
  49. package/dist/runtime/composition-emit-scheduler.d.ts +9 -15
  50. package/dist/runtime/composition-emit-scheduler.js +9 -15
  51. package/dist/runtime/content-language.d.ts +32 -0
  52. package/dist/runtime/content-language.js +46 -0
  53. package/dist/runtime/content-region.d.ts +17 -0
  54. package/dist/runtime/content-region.js +29 -0
  55. package/dist/runtime/content-registration.d.ts +13 -0
  56. package/dist/runtime/content-registration.js +25 -0
  57. package/dist/runtime/core/SectionEngineCore.d.ts +2 -2
  58. package/dist/runtime/core/SectionEngineCore.js +2 -2
  59. package/dist/runtime/core/cohort.d.ts +1 -1
  60. package/dist/runtime/core/cohort.js +1 -1
  61. package/dist/runtime/core/engine-input.d.ts +18 -30
  62. package/dist/runtime/core/engine-input.js +11 -12
  63. package/dist/runtime/core/engine-output.d.ts +16 -40
  64. package/dist/runtime/core/engine-output.js +9 -21
  65. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  66. package/dist/runtime/core/engine-readiness.js +8 -41
  67. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  68. package/dist/runtime/core/engine-resolver.js +18 -43
  69. package/dist/runtime/core/engine-state.d.ts +28 -36
  70. package/dist/runtime/core/engine-state.js +18 -21
  71. package/dist/runtime/core/engine-transition.d.ts +14 -19
  72. package/dist/runtime/core/engine-transition.js +71 -51
  73. package/dist/runtime/engine.d.ts +16 -19
  74. package/dist/runtime/engine.js +12 -19
  75. package/dist/runtime/page-warnings.d.ts +13 -0
  76. package/dist/runtime/page-warnings.js +22 -0
  77. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  78. package/dist/runtime/policy-input-forwarding.js +25 -0
  79. package/dist/runtime/registration-events.d.ts +11 -0
  80. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  81. package/dist/runtime/runtime-event-claim.js +15 -0
  82. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  83. package/dist/runtime/shell-event-bridge.js +127 -0
  84. package/dist/runtime/shell-registration.d.ts +73 -0
  85. package/dist/runtime/shell-registration.js +95 -0
  86. package/dist/runtime/shell-scope.d.ts +68 -0
  87. package/dist/runtime/shell-scope.js +188 -0
  88. package/dist/runtime/tool-host-contract.d.ts +8 -10
  89. package/dist/runtime/tool-host-contract.js +11 -11
  90. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  91. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  92. package/dist/services/ElementToolStateStore.d.ts +14 -10
  93. package/dist/services/ElementToolStateStore.js +37 -13
  94. package/dist/services/HighlightCoordinator.d.ts +21 -21
  95. package/dist/services/HighlightCoordinator.js +431 -325
  96. package/dist/services/RangeSerializer.d.ts +10 -1
  97. package/dist/services/RangeSerializer.js +87 -14
  98. package/dist/services/TTSService.d.ts +117 -70
  99. package/dist/services/TTSService.js +668 -578
  100. package/dist/services/ToolCoordinator.d.ts +43 -60
  101. package/dist/services/ToolCoordinator.js +164 -206
  102. package/dist/services/ToolRegistry.d.ts +74 -57
  103. package/dist/services/ToolRegistry.js +104 -114
  104. package/dist/services/ToolkitCoordinator.d.ts +183 -189
  105. package/dist/services/ToolkitCoordinator.js +477 -431
  106. package/dist/services/audio-handoff.d.ts +6 -7
  107. package/dist/services/audio-handoff.js +20 -14
  108. package/dist/services/catalog-owner.d.ts +0 -1
  109. package/dist/services/framework-error-bus.d.ts +3 -6
  110. package/dist/services/framework-error-bus.js +3 -6
  111. package/dist/services/framework-error.d.ts +22 -7
  112. package/dist/services/framework-error.js +9 -7
  113. package/dist/services/interfaces.d.ts +158 -129
  114. package/dist/services/section-session-assignment.d.ts +21 -0
  115. package/dist/services/section-session-assignment.js +82 -0
  116. package/dist/services/spoken-audio-cards.d.ts +0 -2
  117. package/dist/services/spoken-audio-cards.js +0 -2
  118. package/dist/services/tool-config-defaults.d.ts +3 -3
  119. package/dist/services/tool-config-defaults.js +3 -3
  120. package/dist/services/tool-config-validation.d.ts +4 -5
  121. package/dist/services/tool-config-validation.js +36 -61
  122. package/dist/services/tool-context.d.ts +0 -8
  123. package/dist/services/tool-context.js +0 -12
  124. package/dist/services/tool-icons.d.ts +4 -3
  125. package/dist/services/tool-icons.js +4 -3
  126. package/dist/services/tool-instance-id.d.ts +0 -1
  127. package/dist/services/tool-instance-id.js +0 -6
  128. package/dist/services/tool-providers/TTSToolProvider.d.ts +18 -19
  129. package/dist/services/tool-providers/TTSToolProvider.js +37 -34
  130. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
  131. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +30 -26
  132. package/dist/services/tool-providers/ToolProviderRegistry.js +119 -93
  133. package/dist/services/tool-request.d.ts +27 -12
  134. package/dist/services/tool-request.js +24 -14
  135. package/dist/services/toolbar-decided-tools.d.ts +15 -0
  136. package/dist/services/toolbar-decided-tools.js +28 -0
  137. package/dist/services/tools-config-normalizer.d.ts +17 -22
  138. package/dist/services/tools-config-normalizer.js +2 -10
  139. package/dist/services/tts/browser-provider.d.ts +38 -2
  140. package/dist/services/tts/browser-provider.js +276 -140
  141. package/dist/services/tts/flat-tree.d.ts +71 -0
  142. package/dist/services/tts/flat-tree.js +269 -0
  143. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  144. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  145. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  146. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  147. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  148. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  149. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +12 -4
  150. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  151. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  152. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  153. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  154. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  155. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  156. package/dist/services/tts/math-aware-text-processing.js +32 -18
  157. package/dist/services/tts/math-control-names.d.ts +21 -0
  158. package/dist/services/tts/math-control-names.js +142 -0
  159. package/dist/services/tts/math-speech.d.ts +11 -0
  160. package/dist/services/tts/math-speech.js +51 -18
  161. package/dist/services/tts/provider-options.d.ts +41 -0
  162. package/dist/services/tts/provider-options.js +1 -0
  163. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  164. package/dist/services/tts/ssml/spoken-text.js +29 -0
  165. package/dist/services/tts/start-failure.d.ts +11 -0
  166. package/dist/services/tts/start-failure.js +19 -0
  167. package/dist/services/tts/text-processing.d.ts +9 -0
  168. package/dist/services/tts/text-processing.js +46 -33
  169. package/dist/services/tts/text-segmentation.d.ts +7 -0
  170. package/dist/services/tts/text-segmentation.js +47 -0
  171. package/dist/services/tts-runtime-config.d.ts +25 -17
  172. package/dist/services/tts-runtime-config.js +16 -95
  173. package/dist/tools/content-capability-resolution.d.ts +7 -9
  174. package/dist/tools/content-capability-resolution.js +8 -45
  175. package/dist/tools/registration.d.ts +58 -0
  176. package/dist/tools/registration.js +54 -0
  177. package/dist/tools/registrations/toolbar-registration-helpers.js +2 -2
  178. package/dist/tools/tool-surface-host.d.ts +6 -10
  179. package/dist/tools/tool-surface-host.js +44 -42
  180. package/dist/tools/tool-tag-map.d.ts +5 -15
  181. package/dist/tools/tool-tag-map.js +6 -20
  182. package/dist/utils/structural-equality.d.ts +11 -0
  183. package/dist/utils/structural-equality.js +47 -0
  184. package/package.json +13 -29
  185. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  186. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  187. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  188. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  189. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  190. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  191. package/dist/context/runtime-context-consumer.d.ts +0 -13
  192. package/dist/context/runtime-context-consumer.js +0 -18
  193. package/dist/item-loader.d.ts +0 -44
  194. package/dist/item-loader.js +0 -61
  195. package/dist/policy/core/engine-context.d.ts +0 -29
  196. package/dist/policy/core/engine-context.js +0 -27
  197. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  198. package/dist/policy/core/policy-source-tag.js +0 -28
  199. package/dist/policy/internal.d.ts +0 -25
  200. package/dist/policy/internal.js +0 -25
  201. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  202. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  203. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  204. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  205. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  206. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  207. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  208. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  209. package/dist/runtime/engine-context.d.ts +0 -39
  210. package/dist/runtime/engine-context.js +0 -31
  211. package/dist/runtime/internal.d.ts +0 -43
  212. package/dist/runtime/internal.js +0 -45
  213. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  214. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  215. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  216. package/dist/runtime/stage-emit-gate.js +0 -82
  217. package/dist/services/I18nService.d.ts +0 -47
  218. package/dist/services/I18nService.js +0 -84
  219. package/dist/services/ThemeProvider.d.ts +0 -95
  220. package/dist/services/ThemeProvider.js +0 -334
  221. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  222. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  223. package/dist/services/pnp-standard-features.d.ts +0 -238
  224. package/dist/services/pnp-standard-features.js +0 -338
  225. package/dist/tools/client.d.ts +0 -10
  226. package/dist/tools/client.js +0 -10
  227. package/dist/tools/internal.d.ts +0 -44
  228. package/dist/tools/internal.js +0 -39
@@ -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)) {
@@ -7,3 +7,10 @@ export interface SentenceSegmentationOptions {
7
7
  useSentenceSegmenter?: boolean;
8
8
  }
9
9
  export declare const segmentSentences: (text: string, options?: SentenceSegmentationOptions) => SentenceSegment[];
10
+ /**
11
+ * `text` in pieces of at most `maxLength` characters: whole sentences while they
12
+ * fit, a sentence too long for one piece cut at its last space that fits, and a
13
+ * word too long for one piece cut where the limit falls. Each piece is a trimmed
14
+ * substring of `text`, at its offset there.
15
+ */
16
+ export declare const splitTextToLength: (text: string, maxLength: number, options?: SentenceSegmentationOptions) => SentenceSegment[];
@@ -35,3 +35,50 @@ export const segmentSentences = (text, options) => {
35
35
  }
36
36
  return parsed.length > 0 ? parsed : [{ text, offset: 0 }];
37
37
  };
38
+ /**
39
+ * `text` in pieces of at most `maxLength` characters: whole sentences while they
40
+ * fit, a sentence too long for one piece cut at its last space that fits, and a
41
+ * word too long for one piece cut where the limit falls. Each piece is a trimmed
42
+ * substring of `text`, at its offset there.
43
+ */
44
+ export const splitTextToLength = (text, maxLength, options) => {
45
+ const limit = Math.max(1, Math.floor(maxLength));
46
+ const pieces = [];
47
+ let start = -1;
48
+ let end = -1;
49
+ const flush = () => {
50
+ if (start >= 0)
51
+ pieces.push({ text: text.slice(start, end), offset: start });
52
+ start = -1;
53
+ };
54
+ const append = (spanStart, spanEnd) => {
55
+ if (start >= 0 && spanEnd - start <= limit) {
56
+ end = spanEnd;
57
+ return;
58
+ }
59
+ flush();
60
+ let cursor = spanStart;
61
+ while (spanEnd - cursor > limit) {
62
+ const window = text.slice(cursor, cursor + limit + 1);
63
+ const space = window.search(/\s\S*$/);
64
+ const cut = space > 0 ? cursor + space : cursor + limit;
65
+ pieces.push({ text: text.slice(cursor, cut).trimEnd(), offset: cursor });
66
+ cursor = cut;
67
+ while (cursor < spanEnd && /\s/.test(text[cursor]))
68
+ cursor++;
69
+ }
70
+ if (cursor < spanEnd) {
71
+ start = cursor;
72
+ end = spanEnd;
73
+ }
74
+ };
75
+ for (const sentence of segmentSentences(text, options)) {
76
+ const leading = sentence.text.search(/\S/);
77
+ if (leading === -1)
78
+ continue;
79
+ const spanStart = sentence.offset + leading;
80
+ append(spanStart, sentence.offset + sentence.text.trimEnd().length);
81
+ }
82
+ flush();
83
+ return pieces;
84
+ };
@@ -1,5 +1,5 @@
1
1
  import type { TTSConfig } from "./TTSService.js";
2
- import type { TextToSpeechToolProviderConfig } from "./tools-config-normalizer.js";
2
+ import type { TextToSpeechToolProviderConfig, ToolProviderConfig } from "./tools-config-normalizer.js";
3
3
  import { type SREMathSpeechOptions } from "./tts/math-speech.js";
4
4
  export type TTSLayoutMode = "reserved-row" | "expanding-row" | "floating-overlay" | "left-aligned";
5
5
  export interface TTSHostToolbarLayout {
@@ -26,9 +26,10 @@ export interface NormalizedTTSSpeedOption {
26
26
  isDefault: boolean;
27
27
  }
28
28
  export interface TTSRuntimeSettings {
29
- backend?: "browser" | "polly" | "google" | "server";
29
+ /** `browser` speaks through the Web Speech API; `server` through a TTS server. */
30
+ backend?: "browser" | "server";
31
+ /** The service behind a `server` backend. */
30
32
  serverProvider?: "polly" | "google" | "custom";
31
- provider?: "polly" | "google" | "custom";
32
33
  engine?: "standard" | "neural";
33
34
  sampleRate?: number;
34
35
  format?: "mp3" | "ogg" | "pcm";
@@ -37,6 +38,15 @@ export interface TTSRuntimeSettings {
37
38
  rate?: number;
38
39
  pitch?: number;
39
40
  apiEndpoint?: string;
41
+ /**
42
+ * The host's locale, used for a read whose content names no language: it
43
+ * drives text normalization and segmentation, is sent as `language` on the
44
+ * `pie` transport and as `lang_id` on the `custom` transport when nothing
45
+ * names a language, and a server given no voice picks one for it. The
46
+ * content language a read resolves (markup `lang`, the tool's `language`,
47
+ * the toolkit's `content-language`, then `lang_id`) wins over it.
48
+ * `docs/architecture/internationalization.md#tts-language` is the contract.
49
+ */
40
50
  language?: string;
41
51
  transportMode?: "pie" | "custom";
42
52
  endpointMode?: "synthesizePath" | "rootPost";
@@ -51,6 +61,12 @@ export interface TTSRuntimeSettings {
51
61
  validateEndpoint?: boolean;
52
62
  cache?: boolean;
53
63
  speedRate?: "slow" | "medium" | "fast";
64
+ /**
65
+ * The custom transport's locale, sent on every read in place of the content
66
+ * language. Where markup and the tool name no language it is also the read's
67
+ * content language, so text processing and catalog lookups follow it. Leave
68
+ * it unset to let content language reach the service.
69
+ */
54
70
  lang_id?: string;
55
71
  /**
56
72
  * Optional inline TTS speed buttons.
@@ -88,17 +104,6 @@ export interface TTSRuntimeSettings {
88
104
  providerOptions?: Record<string, unknown>;
89
105
  }
90
106
  export declare const normalizeTTSLayoutMode: (value: unknown, fallback?: TTSLayoutMode) => TTSLayoutMode;
91
- /** Legacy numeric helper defaults. Rendered controls add visible Normal separately. */
92
- export declare const DEFAULT_TTS_SPEED_OPTIONS: readonly number[];
93
- export declare const normalizeTTSSpeedOptionConfigs: (value: unknown) => TTSSpeedOption[];
94
- /**
95
- * Resolves inline toolbar speed options from host config.
96
- * - Omitted/non-array: default speed buttons.
97
- * - Empty array: hide speed buttons.
98
- * - Arrays that sanitize to no valid values: default speed buttons.
99
- * - 1.0× is never shown as a discrete speed button.
100
- */
101
- export declare const normalizeTTSSpeedOptions: (value: unknown) => number[];
102
107
  export declare const normalizeTTSSpeedControlOptions: (value: unknown) => NormalizedTTSSpeedOption[];
103
108
  export declare const formatTTSSpeedOptionsAsText: (values: number[]) => string;
104
109
  /**
@@ -107,10 +112,13 @@ export declare const formatTTSSpeedOptionsAsText: (values: number[]) => string;
107
112
  * - Non-empty text with no parseable finite numbers → same as invalid-only array config: defaults.
108
113
  */
109
114
  export declare const parseTTSSpeedOptionsFromText: (text: string) => number[];
110
- export declare const resolveTTSRuntimeSettings: (config: TextToSpeechToolProviderConfig | TTSRuntimeSettings | undefined) => TTSRuntimeSettings;
115
+ /**
116
+ * The runtime settings of a `textToSpeech` provider entry: its top-level keys
117
+ * with defaults applied. `enabled` and the runtime `provider` object belong to
118
+ * the tool registration and are dropped.
119
+ */
120
+ export declare const resolveTTSRuntimeSettings: (config: TextToSpeechToolProviderConfig | ToolProviderConfig | TTSRuntimeSettings | undefined) => TTSRuntimeSettings;
111
121
  export declare const resolveTTSBackend: (config: TTSRuntimeSettings) => NonNullable<TTSRuntimeSettings["backend"]>;
112
- export declare const resolveRuntimeProvider: (config: TTSRuntimeSettings, backend: NonNullable<TTSRuntimeSettings["backend"]>) => TTSRuntimeSettings["serverProvider"];
113
- export declare const resolveTransportMode: (config: TTSRuntimeSettings, runtimeProvider: TTSRuntimeSettings["serverProvider"]) => NonNullable<TTSRuntimeSettings["transportMode"]>;
114
122
  /**
115
123
  * The provider configuration built from host settings: the contract's portable
116
124
  * fields plus the server adapter's, under the adapter's names.
@@ -6,26 +6,16 @@ const VALID_TTS_LAYOUT_MODES = new Set([
6
6
  "left-aligned",
7
7
  ]);
8
8
  const toRecord = (value) => value && typeof value === "object" ? value : {};
9
- const isServerBackend = (backend) => backend === "polly" || backend === "google" || backend === "server";
10
9
  const withDefault = (value, fallback) => value === undefined ? fallback : value;
11
10
  export const normalizeTTSLayoutMode = (value, fallback = "left-aligned") => typeof value === "string" &&
12
11
  VALID_TTS_LAYOUT_MODES.has(value)
13
12
  ? value
14
13
  : fallback;
15
- /** Legacy numeric helper defaults. Rendered controls add visible Normal separately. */
16
- export const DEFAULT_TTS_SPEED_OPTIONS = Object.freeze([0.8, 1.25]);
17
14
  const DEFAULT_TTS_SPEED_CONTROL_OPTIONS = Object.freeze([
18
15
  { rate: 0.8, label: "Slow", ariaLabel: "Slow speed" },
19
16
  { rate: 1, label: "Normal", ariaLabel: "Normal speed", default: true },
20
17
  { rate: 1.25, label: "Fast", ariaLabel: "Fast speed" },
21
18
  ]);
22
- const normalizeSpeedRate = (entry) => {
23
- if (typeof entry !== "number" || !Number.isFinite(entry) || entry <= 0) {
24
- return undefined;
25
- }
26
- const rounded = Math.round(entry * 100) / 100;
27
- return rounded === 1 ? undefined : rounded;
28
- };
29
19
  const normalizeControlSpeedRate = (entry) => {
30
20
  if (typeof entry !== "number" || !Number.isFinite(entry) || entry <= 0) {
31
21
  return undefined;
@@ -51,56 +41,6 @@ const normalizeSpeedAriaLabel = (label, ariaLabel, usedDefaultLabel) => {
51
41
  return ariaLabel;
52
42
  return `${label} ${ariaLabel}`;
53
43
  };
54
- const normalizeTTSSpeedOptionConfig = (entry) => {
55
- if (typeof entry === "number")
56
- return normalizeSpeedRate(entry);
57
- if (!entry || typeof entry !== "object" || Array.isArray(entry))
58
- return undefined;
59
- const record = entry;
60
- const rate = normalizeSpeedRate(record.rate);
61
- if (rate === undefined)
62
- return undefined;
63
- const label = trimOptionalText(record.label);
64
- const ariaLabel = trimOptionalText(record.ariaLabel);
65
- return {
66
- rate,
67
- ...(label ? { label } : {}),
68
- ...(ariaLabel ? { ariaLabel } : {}),
69
- };
70
- };
71
- export const normalizeTTSSpeedOptionConfigs = (value) => {
72
- if (!Array.isArray(value))
73
- return [...DEFAULT_TTS_SPEED_OPTIONS];
74
- if (value.length === 0)
75
- return [];
76
- const dedupedRates = new Set();
77
- const normalized = [];
78
- for (const entry of value) {
79
- const option = normalizeTTSSpeedOptionConfig(entry);
80
- if (option === undefined)
81
- continue;
82
- const rate = typeof option === "number" ? option : option.rate;
83
- if (dedupedRates.has(rate))
84
- continue;
85
- dedupedRates.add(rate);
86
- normalized.push(option);
87
- }
88
- return normalized.length ? normalized : [...DEFAULT_TTS_SPEED_OPTIONS];
89
- };
90
- /**
91
- * Resolves inline toolbar speed options from host config.
92
- * - Omitted/non-array: default speed buttons.
93
- * - Empty array: hide speed buttons.
94
- * - Arrays that sanitize to no valid values: default speed buttons.
95
- * - 1.0× is never shown as a discrete speed button.
96
- */
97
- export const normalizeTTSSpeedOptions = (value) => {
98
- if (!Array.isArray(value))
99
- return [...DEFAULT_TTS_SPEED_OPTIONS];
100
- if (value.length === 0)
101
- return [];
102
- return normalizeTTSSpeedOptionConfigs(value).map((option) => typeof option === "number" ? option : option.rate);
103
- };
104
44
  export const normalizeTTSSpeedControlOptions = (value) => {
105
45
  const input = Array.isArray(value)
106
46
  ? value
@@ -203,8 +143,7 @@ const applyRuntimeDefaults = (config) => {
203
143
  ...config,
204
144
  layoutMode: normalizeTTSLayoutMode(config.layoutMode),
205
145
  };
206
- const backend = config.backend || "browser";
207
- if (!isServerBackend(backend))
146
+ if (config.backend !== "server")
208
147
  return withLayoutDefaults;
209
148
  const withServerDefaults = {
210
149
  ...withLayoutDefaults,
@@ -216,48 +155,30 @@ const applyRuntimeDefaults = (config) => {
216
155
  rate: withDefault(withLayoutDefaults.rate, 1.0),
217
156
  language: withDefault(withLayoutDefaults.language, "en-US"),
218
157
  };
219
- if (backend === "polly") {
158
+ if (config.serverProvider === "polly") {
220
159
  return {
221
160
  ...withServerDefaults,
222
- defaultVoice: withDefault(withServerDefaults.defaultVoice, "Joanna"),
223
161
  engine: withDefault(withServerDefaults.engine, "neural"),
224
162
  format: withDefault(withServerDefaults.format, "mp3"),
225
163
  speechMarksMode: withDefault(withServerDefaults.speechMarksMode, "word+sentence"),
226
164
  };
227
165
  }
228
- if (backend === "google") {
229
- return {
230
- ...withServerDefaults,
231
- defaultVoice: withDefault(withServerDefaults.defaultVoice, "en-US-Wavenet-A"),
232
- };
233
- }
234
166
  return withServerDefaults;
235
167
  };
168
+ /**
169
+ * The runtime settings of a `textToSpeech` provider entry: its top-level keys
170
+ * with defaults applied. `enabled` and the runtime `provider` object belong to
171
+ * the tool registration and are dropped.
172
+ */
236
173
  export const resolveTTSRuntimeSettings = (config) => {
237
- const configRecord = toRecord(config);
238
- const settingsRecord = toRecord(configRecord.settings);
239
- const { provider, ...merged } = { ...configRecord, ...settingsRecord };
240
- // A runtime provider object in `provider` is for the tool registration; the
241
- // runtime settings carry only a server provider id.
242
- return applyRuntimeDefaults({
243
- ...merged,
244
- ...(typeof provider === "string" ? { provider } : {}),
245
- });
174
+ const { enabled: _enabled, provider: _provider, ...settings } = toRecord(config);
175
+ return applyRuntimeDefaults(settings);
246
176
  };
247
177
  export const resolveTTSBackend = (config) => config.backend || "browser";
248
- export const resolveRuntimeProvider = (config, backend) => {
249
- if (backend === "polly" || backend === "google")
250
- return backend;
251
- if (backend === "server") {
252
- return config.serverProvider || config.provider;
253
- }
254
- return config.serverProvider || config.provider;
255
- };
256
- export const resolveTransportMode = (config, runtimeProvider) => config.transportMode || (runtimeProvider === "custom" ? "custom" : "pie");
257
178
  export const buildRuntimeTTSConfig = (config) => {
258
- const backend = resolveTTSBackend(config);
259
- const runtimeProvider = resolveRuntimeProvider(config, backend);
260
- const transportMode = resolveTransportMode(config, runtimeProvider);
179
+ const runtimeProvider = resolveTTSBackend(config) === "server" ? config.serverProvider : undefined;
180
+ const polly = runtimeProvider === "polly";
181
+ const transportMode = config.transportMode || (runtimeProvider === "custom" ? "custom" : "pie");
261
182
  const mathSpeech = normalizeSREMathSpeechOptions(config.mathSpeech);
262
183
  const runtimeConfig = {
263
184
  voice: config.defaultVoice,
@@ -266,16 +187,16 @@ export const buildRuntimeTTSConfig = (config) => {
266
187
  providerOptions: {
267
188
  ...toRecord(config.providerOptions),
268
189
  ...(config.language ? { locale: config.language } : {}),
269
- ...(backend === "polly" && config.engine
190
+ ...(polly && config.engine
270
191
  ? { engine: config.engine }
271
192
  : {}),
272
- ...(backend === "polly" && typeof config.sampleRate === "number"
193
+ ...(polly && typeof config.sampleRate === "number"
273
194
  ? { sampleRate: config.sampleRate }
274
195
  : {}),
275
- ...(backend === "polly" && config.format
196
+ ...(polly && config.format
276
197
  ? { format: config.format }
277
198
  : {}),
278
- ...(backend === "polly"
199
+ ...(polly
279
200
  ? {
280
201
  speechMarkTypes: config.speechMarksMode === "word+sentence"
281
202
  ? ["word", "sentence"]
@@ -3,7 +3,7 @@
3
3
  * profile.
4
4
  *
5
5
  * Two independent halves, and a capability is in play only when both answer yes:
6
- * policy granted one of its support ids, and its own `requiresAuthoredContent`
6
+ * policy granted its support id, and its own `requiresAuthoredContent`
7
7
  * found the resource in this entity's catalogs. Neither implies the other — a
8
8
  * learner with an accommodation still sees nothing on an item carrying no
9
9
  * resource, and an item carrying one shows nothing to a learner without the
@@ -30,11 +30,9 @@
30
30
  */
31
31
  import type { ToolRegistration } from "../services/ToolRegistry.js";
32
32
  import type { CatalogOwnerSnapshot } from "../services/AccessibilityCatalogResolver.js";
33
- /** What policy answered about one feature id. */
33
+ /** What policy answered about one capability's support id. */
34
34
  export type ContentCapabilityPolicy = {
35
35
  outcome: "granted";
36
- /** The support id that was granted — a capability may declare several. */
37
- featureId: string;
38
36
  /** Feature parameters carried by the decision, if any. */
39
37
  parameters?: unknown;
40
38
  }
@@ -61,12 +59,12 @@ export type ContentCapabilityPhase = "policy" | "content";
61
59
  export interface ResolvedContentCapability {
62
60
  registration: ToolRegistration;
63
61
  /**
64
- * The granted support id, or `""` when the capability resolved without a
65
- * grant. Passed through to the render context unchanged, so a capability that
62
+ * Whether policy granted the capability; `false` when it resolved from content
63
+ * alone. Passed through to the render context unchanged, so a capability that
66
64
  * serves both an authored-presentation case and an accommodation can still
67
65
  * tell them apart at render time.
68
66
  */
69
- featureId: string;
67
+ granted: boolean;
70
68
  parameters?: unknown;
71
69
  /** Whatever the capability's own `resolve` returned; never inspected here. */
72
70
  content: unknown;
@@ -81,7 +79,7 @@ export interface ResolveContentCapabilitiesArgs {
81
79
  /** The entity's cards, or `null` when no resolver is available. */
82
80
  catalogs: CatalogOwnerSnapshot | null;
83
81
  /**
84
- * Policy's answer about one feature id, in three states.
82
+ * Policy's answer about one support id, which is a tool id, in three states.
85
83
  *
86
84
  * Granting requires a documented need, so an unconfigured feature is
87
85
  * `"silent"`, never granted. What the third state buys is the distinction
@@ -89,7 +87,7 @@ export interface ResolveContentCapabilitiesArgs {
89
87
  * something, and a capability allowed to answer from content alone must not
90
88
  * treat that as nobody having spoken.
91
89
  */
92
- policyFor: (featureId: string) => ContentCapabilityPolicy;
90
+ policyFor: (supportId: string) => ContentCapabilityPolicy;
93
91
  /**
94
92
  * Report a capability that threw. It is dropped either way; this is how a
95
93
  * caller surfaces it as its own recoverable warning rather than letting one
@@ -3,7 +3,7 @@
3
3
  * profile.
4
4
  *
5
5
  * Two independent halves, and a capability is in play only when both answer yes:
6
- * policy granted one of its support ids, and its own `requiresAuthoredContent`
6
+ * policy granted its support id, and its own `requiresAuthoredContent`
7
7
  * found the resource in this entity's catalogs. Neither implies the other — a
8
8
  * learner with an accommodation still sees nothing on an item carrying no
9
9
  * resource, and an item carrying one shows nothing to a learner without the
@@ -28,51 +28,15 @@
28
28
  * a capability interprets its own cards, and the resolution owns only the
29
29
  * two-halves rule and its one documented exception.
30
30
  */
31
- /** The support ids a capability answers to, defaulting to its own id. */
32
- const supportIdsOf = (registration) => registration.pnpSupportIds?.length
33
- ? registration.pnpSupportIds
34
- : [registration.toolId];
35
- /**
36
- * Everything policy has to say about one capability: a grant, an off switch, or
37
- * nothing.
38
- *
39
- * Denial is checked ahead of a grant on each id rather than after the scan,
40
- * because the two can only disagree when a host blocked one of a capability's ids
41
- * while a profile granted another, and there the off switch is the later, more
42
- * specific statement about this delivery.
43
- *
44
- * A host gate names *capabilities*, so the tool id is probed too when it is not
45
- * already a declared support id — otherwise a capability whose id differs from
46
- * its support ids would slip a host block. That probe is gate-only: a grant on
47
- * the tool id is ignored, or blocking would double as a second way to switch a
48
- * capability on.
49
- */
50
- function policyForCapability(registration, args) {
51
- const supportIds = supportIdsOf(registration);
52
- for (const supportId of supportIds) {
53
- const answer = args.policyFor(supportId);
54
- if (answer.outcome === "denied")
55
- return { grant: null, denied: true };
56
- if (answer.outcome === "granted")
57
- return { grant: answer, denied: false };
58
- }
59
- if (supportIds.includes(registration.toolId)) {
60
- return { grant: null, denied: false };
61
- }
62
- return {
63
- grant: null,
64
- denied: args.policyFor(registration.toolId).outcome === "denied",
65
- };
66
- }
67
31
  function resolveOne(registration, args) {
68
32
  let grant = null;
69
33
  try {
70
- const answer = policyForCapability(registration, args);
71
- // Nothing reopens a host denial — not a grant it outranked, and not the
72
- // content exception below.
73
- if (answer.denied)
34
+ const answer = args.policyFor(registration.toolId);
35
+ // Nothing reopens a host denial, not even the content exception below.
36
+ if (answer.outcome === "denied")
74
37
  return null;
75
- grant = answer.grant;
38
+ if (answer.outcome === "granted")
39
+ grant = answer;
76
40
  }
77
41
  catch (error) {
78
42
  args.onError?.(registration, "policy", error);
@@ -92,13 +56,12 @@ function resolveOne(registration, args) {
92
56
  return null;
93
57
  return {
94
58
  registration,
95
- featureId: grant.featureId,
59
+ granted: true,
96
60
  parameters: grant.parameters,
97
61
  content: null,
98
62
  };
99
63
  }
100
64
  const context = {
101
- featureId: grant?.featureId ?? "",
102
65
  parameters: grant?.parameters,
103
66
  catalogs: args.catalogs,
104
67
  granted: Boolean(grant),
@@ -117,7 +80,7 @@ function resolveOne(registration, args) {
117
80
  return null;
118
81
  return {
119
82
  registration,
120
- featureId: context.featureId,
83
+ granted: context.granted,
121
84
  parameters: context.parameters,
122
85
  content,
123
86
  };