@pie-players/pie-assessment-toolkit 0.3.75 → 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 (142) hide show
  1. package/README.md +449 -541
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +0 -1
  4. package/dist/attempt/TestSession.js +10 -5
  5. package/dist/components/ItemScope.custom-element.js +1 -1
  6. package/dist/components/ItemToolBar.custom-element.js +1 -1
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +8 -8
  8. package/dist/components/SectionToolBar.custom-element.js +1 -1
  9. package/dist/components/chunks/{ItemToolBar-7tt7v2xp.js → ItemToolBar-3zrc4p3d.js} +4 -4
  10. package/dist/components/chunks/{ItemToolBar-jx38w99s.js → ItemToolBar-66hcwmfw.js} +2 -2
  11. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
  12. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  13. package/dist/components/chunks/{ItemToolBar-97hx60z4.js → ItemToolBar-kkj69ptr.js} +1 -1
  14. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  15. package/dist/context/assessment-toolkit-context.d.ts +6 -0
  16. package/dist/index.d.ts +7 -7
  17. package/dist/index.js +4 -10
  18. package/dist/policy/core/PolicySource.d.ts +1 -1
  19. package/dist/policy/core/ToolPolicyEngine.d.ts +46 -7
  20. package/dist/policy/core/ToolPolicyEngine.js +73 -16
  21. package/dist/policy/core/compose-decision.d.ts +3 -3
  22. package/dist/policy/core/compose-decision.js +30 -30
  23. package/dist/policy/core/decision-types.d.ts +44 -32
  24. package/dist/policy/core/feature-decision.d.ts +17 -15
  25. package/dist/policy/core/feature-decision.js +11 -13
  26. package/dist/policy/core/pnp-policy-inputs.d.ts +5 -2
  27. package/dist/policy/core/pnp-policy-inputs.js +12 -32
  28. package/dist/policy/core/provenance.d.ts +1 -1
  29. package/dist/policy/core/tool-parameters.d.ts +11 -0
  30. package/dist/policy/core/tool-parameters.js +15 -0
  31. package/dist/policy/engine.d.ts +1 -1
  32. package/dist/policy/sources/PnpPolicySource.d.ts +14 -22
  33. package/dist/policy/sources/PnpPolicySource.js +30 -28
  34. package/dist/runtime/SectionControllerBinding.d.ts +6 -2
  35. package/dist/runtime/SectionControllerBinding.js +21 -15
  36. package/dist/runtime/adapter/subscriber-fanout.d.ts +0 -1
  37. package/dist/runtime/adapter/subscriber-fanout.js +1 -4
  38. package/dist/runtime/composition-emit-scheduler.d.ts +3 -3
  39. package/dist/runtime/composition-emit-scheduler.js +1 -1
  40. package/dist/runtime/content-region.d.ts +0 -4
  41. package/dist/runtime/content-region.js +2 -2
  42. package/dist/runtime/core/SectionEngineCore.d.ts +0 -2
  43. package/dist/runtime/core/SectionEngineCore.js +0 -4
  44. package/dist/runtime/core/cohort.d.ts +0 -5
  45. package/dist/runtime/core/cohort.js +0 -9
  46. package/dist/runtime/core/engine-output.d.ts +3 -2
  47. package/dist/runtime/core/engine-transition.js +3 -2
  48. package/dist/runtime/registration-events.d.ts +11 -14
  49. package/dist/runtime/runtime-id.d.ts +1 -1
  50. package/dist/runtime/runtime-id.js +1 -1
  51. package/dist/runtime/shell-scope.d.ts +4 -4
  52. package/dist/runtime/shell-scope.js +15 -9
  53. package/dist/runtime/tool-host-contract.d.ts +0 -4
  54. package/dist/runtime/tool-host-contract.js +1 -1
  55. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  56. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  57. package/dist/services/ElementToolStateStore.d.ts +32 -61
  58. package/dist/services/ElementToolStateStore.js +37 -67
  59. package/dist/services/HighlightCoordinator.d.ts +9 -21
  60. package/dist/services/HighlightCoordinator.js +52 -80
  61. package/dist/services/RangeSerializer.d.ts +4 -35
  62. package/dist/services/RangeSerializer.js +4 -48
  63. package/dist/services/SSMLExtractor.d.ts +8 -31
  64. package/dist/services/SSMLExtractor.js +18 -40
  65. package/dist/services/TTSService.d.ts +49 -56
  66. package/dist/services/TTSService.js +308 -283
  67. package/dist/services/ToolCoordinator.d.ts +6 -49
  68. package/dist/services/ToolCoordinator.js +25 -61
  69. package/dist/services/ToolRegistry.d.ts +82 -80
  70. package/dist/services/ToolRegistry.js +91 -67
  71. package/dist/services/ToolkitCoordinator.d.ts +83 -81
  72. package/dist/services/ToolkitCoordinator.js +186 -174
  73. package/dist/services/framework-error.d.ts +30 -7
  74. package/dist/services/framework-error.js +10 -13
  75. package/dist/services/interfaces.d.ts +47 -71
  76. package/dist/services/section-controller-types.d.ts +7 -7
  77. package/dist/services/section-session-assignment.js +6 -8
  78. package/dist/services/spoken-audio-cards.d.ts +2 -4
  79. package/dist/services/spoken-audio-cards.js +2 -4
  80. package/dist/services/tool-config-validation.d.ts +5 -7
  81. package/dist/services/tool-config-validation.js +11 -26
  82. package/dist/services/tool-context.d.ts +7 -23
  83. package/dist/services/tool-context.js +99 -53
  84. package/dist/services/tool-failure.d.ts +30 -0
  85. package/dist/services/tool-failure.js +11 -0
  86. package/dist/services/tool-icons.d.ts +1 -2
  87. package/dist/services/tool-icons.js +2 -2
  88. package/dist/services/tool-instance-id.d.ts +8 -6
  89. package/dist/services/tool-instance-id.js +12 -18
  90. package/dist/services/tool-providers/TTSToolProvider.d.ts +6 -43
  91. package/dist/services/tool-providers/TTSToolProvider.js +7 -63
  92. package/dist/services/tool-providers/ToolProviderApi.d.ts +0 -7
  93. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +7 -42
  94. package/dist/services/tool-providers/ToolProviderRegistry.js +21 -63
  95. package/dist/services/tool-request.d.ts +4 -0
  96. package/dist/services/tool-request.js +7 -2
  97. package/dist/services/toolbar-decided-tools.d.ts +1 -0
  98. package/dist/services/toolbar-decided-tools.js +1 -0
  99. package/dist/services/toolbar-items.js +2 -5
  100. package/dist/services/tools-config-normalizer.d.ts +18 -9
  101. package/dist/services/tools-config-normalizer.js +25 -19
  102. package/dist/services/tts/browser-provider.d.ts +2 -5
  103. package/dist/services/tts/browser-provider.js +4 -20
  104. package/dist/services/tts/catalog-span-alignment.js +14 -7
  105. package/dist/services/tts/flat-tree.d.ts +0 -7
  106. package/dist/services/tts/flat-tree.js +2 -2
  107. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  108. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  109. package/dist/services/tts/generated-speech/index.js +1 -1
  110. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  111. package/dist/services/tts/generated-speech/types.js +1 -1
  112. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +10 -12
  113. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  114. package/dist/services/tts/math-aware-text-processing.js +10 -29
  115. package/dist/services/tts/math-speech.d.ts +1 -1
  116. package/dist/services/tts/math-speech.js +3 -0
  117. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  118. package/dist/services/tts/mathml-sanitization.js +9 -1
  119. package/dist/services/tts/provider-options.d.ts +12 -1
  120. package/dist/services/tts/ssml/spoken-text.js +1 -1
  121. package/dist/services/tts/start-failure.d.ts +0 -7
  122. package/dist/services/tts/start-failure.js +1 -1
  123. package/dist/services/tts/text-segmentation.d.ts +5 -0
  124. package/dist/services/tts/text-segmentation.js +13 -10
  125. package/dist/services/tts-runtime-config.d.ts +32 -12
  126. package/dist/services/tts-runtime-config.js +14 -20
  127. package/dist/tools/content-capability-resolution.d.ts +2 -2
  128. package/dist/tools/registration.d.ts +6 -5
  129. package/dist/tools/registration.js +10 -9
  130. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  131. package/dist/tools/registrations/toolbar-registration-helpers.js +6 -0
  132. package/dist/tools/tool-surface-host.js +1 -8
  133. package/package.json +8 -6
  134. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +0 -2
  135. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +0 -40
  136. package/dist/components/chunks/sre-engine-jd8cc08x.js +0 -1
  137. package/dist/services/ContextVariableStore.d.ts +0 -113
  138. package/dist/services/ContextVariableStore.js +0 -206
  139. package/dist/services/tts-control-events.d.ts +0 -13
  140. package/dist/services/tts-control-events.js +0 -7
  141. package/dist/utils/logger.d.ts +0 -6
  142. package/dist/utils/logger.js +0 -10
@@ -7,7 +7,8 @@
7
7
  * Uses CSS selector paths and node indices for robust serialization that
8
8
  * survives content changes when possible. A path into an open shadow root names
9
9
  * its host, then ` >>> `, then the path inside the root, so ranges in content
10
- * that renders into shadow roots round-trip; paths without one read as before.
10
+ * that renders into shadow roots round-trip; a path with no ` >>> ` is a plain
11
+ * selector path.
11
12
  */
12
13
  /**
13
14
  * Serialized form of a Range suitable for storage.
@@ -79,19 +80,9 @@ export declare class RangeSerializer {
79
80
  * Uses a hybrid approach:
80
81
  * - For element nodes: CSS selector path
81
82
  * - For text nodes: parent selector + text node index
82
- *
83
- * @param node - Node to get path for
84
- * @param root - Root element
85
- * @returns Path string
86
83
  */
87
84
  private getNodePath;
88
- /**
89
- * Get CSS selector path to an element.
90
- *
91
- * @param element - Element to get path for
92
- * @param root - Root element
93
- * @returns CSS selector path
94
- */
85
+ /** Get CSS selector path to an element. */
95
86
  private getElementPath;
96
87
  /** The path to `shadowRoot` itself: its host's path and an empty step into it. */
97
88
  private getShadowRootPath;
@@ -100,28 +91,6 @@ export declare class RangeSerializer {
100
91
  * {@link SHADOW_STEP} continues in the shadow root of what the step before named.
101
92
  */
102
93
  private resolveElementPath;
103
- /**
104
- * Find a node by its path from root.
105
- *
106
- * @param path - Path string from getNodePath
107
- * @param root - Root element
108
- * @returns Node, or null if not found
109
- */
94
+ /** The node a getNodePath path names, or null if not found. */
110
95
  private findNodeByPath;
111
- /**
112
- * Batch serialize multiple ranges.
113
- *
114
- * @param ranges - Ranges to serialize
115
- * @param root - Root element
116
- * @returns Array of serialized ranges
117
- */
118
- serializeMany(ranges: Range[], root: Element): SerializedRange[];
119
- /**
120
- * Batch deserialize multiple ranges.
121
- *
122
- * @param data - Array of serialized ranges
123
- * @param root - Root element
124
- * @returns Array of ranges (nulls for invalid ranges)
125
- */
126
- deserializeMany(data: SerializedRange[], root: Element): (Range | null)[];
127
96
  }
@@ -7,7 +7,8 @@
7
7
  * Uses CSS selector paths and node indices for robust serialization that
8
8
  * survives content changes when possible. A path into an open shadow root names
9
9
  * its host, then ` >>> `, then the path inside the root, so ranges in content
10
- * that renders into shadow roots round-trip; paths without one read as before.
10
+ * that renders into shadow roots round-trip; a path with no ` >>> ` is a plain
11
+ * selector path.
11
12
  */
12
13
  import { isShadowRootNode } from "./tts/flat-tree.js";
13
14
  /** Separates a shadow host's path from the path inside its shadow root. */
@@ -69,11 +70,9 @@ export class RangeSerializer {
69
70
  const range = new Range();
70
71
  range.setStart(startNode, data.startOffset);
71
72
  range.setEnd(endNode, data.endOffset);
72
- // Validate text hasn't changed
73
73
  if (range.toString() === data.text) {
74
74
  return range;
75
75
  }
76
- // Text changed, range is invalid
77
76
  return null;
78
77
  }
79
78
  catch (error) {
@@ -88,22 +87,16 @@ export class RangeSerializer {
88
87
  * Uses a hybrid approach:
89
88
  * - For element nodes: CSS selector path
90
89
  * - For text nodes: parent selector + text node index
91
- *
92
- * @param node - Node to get path for
93
- * @param root - Root element
94
- * @returns Path string
95
90
  */
96
91
  getNodePath(node, root) {
97
92
  if (node === root) {
98
93
  return "";
99
94
  }
100
- // Handle text nodes
101
95
  if (node.nodeType === Node.TEXT_NODE) {
102
96
  const parent = node.parentNode;
103
97
  if (!parent) {
104
98
  throw new Error("Text node has no parent");
105
99
  }
106
- // Get index of this text node among its siblings
107
100
  const textNodes = Array.from(parent.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE);
108
101
  const index = textNodes.indexOf(node);
109
102
  const parentPath = isShadowRootNode(parent)
@@ -111,7 +104,6 @@ export class RangeSerializer {
111
104
  : this.getElementPath(parent, root);
112
105
  return `${parentPath}::text[${index}]`;
113
106
  }
114
- // Handle element nodes
115
107
  if (node.nodeType === Node.ELEMENT_NODE) {
116
108
  return this.getElementPath(node, root);
117
109
  }
@@ -120,13 +112,7 @@ export class RangeSerializer {
120
112
  }
121
113
  throw new Error(`Unsupported node type: ${node.nodeType}`);
122
114
  }
123
- /**
124
- * Get CSS selector path to an element.
125
- *
126
- * @param element - Element to get path for
127
- * @param root - Root element
128
- * @returns CSS selector path
129
- */
115
+ /** Get CSS selector path to an element. */
130
116
  getElementPath(element, root) {
131
117
  if (element === root) {
132
118
  return "";
@@ -207,24 +193,16 @@ export class RangeSerializer {
207
193
  }
208
194
  return scope;
209
195
  }
210
- /**
211
- * Find a node by its path from root.
212
- *
213
- * @param path - Path string from getNodePath
214
- * @param root - Root element
215
- * @returns Node, or null if not found
216
- */
196
+ /** The node a getNodePath path names, or null if not found. */
217
197
  findNodeByPath(path, root) {
218
198
  if (path === "") {
219
199
  return root;
220
200
  }
221
- // Handle text node paths
222
201
  if (path.includes("::text[")) {
223
202
  const marker = path.lastIndexOf("::text[");
224
203
  const elementPath = path.slice(0, marker);
225
204
  const textPart = path.slice(marker + "::text[".length);
226
205
  const textIndex = Number.parseInt(textPart.replace("]", ""), 10);
227
- // Find parent element
228
206
  let parent;
229
207
  try {
230
208
  parent = this.resolveElementPath(elementPath, root);
@@ -235,11 +213,9 @@ export class RangeSerializer {
235
213
  }
236
214
  if (!parent)
237
215
  return null;
238
- // Find text node by index
239
216
  const textNodes = Array.from(parent.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE);
240
217
  return textNodes[textIndex] || null;
241
218
  }
242
- // Handle element paths
243
219
  try {
244
220
  return this.resolveElementPath(path, root);
245
221
  }
@@ -248,24 +224,4 @@ export class RangeSerializer {
248
224
  return null;
249
225
  }
250
226
  }
251
- /**
252
- * Batch serialize multiple ranges.
253
- *
254
- * @param ranges - Ranges to serialize
255
- * @param root - Root element
256
- * @returns Array of serialized ranges
257
- */
258
- serializeMany(ranges, root) {
259
- return ranges.map((range) => this.serialize(range, root));
260
- }
261
- /**
262
- * Batch deserialize multiple ranges.
263
- *
264
- * @param data - Array of serialized ranges
265
- * @param root - Root element
266
- * @returns Array of ranges (nulls for invalid ranges)
267
- */
268
- deserializeMany(data, root) {
269
- return data.map((item) => this.deserialize(item, root));
270
- }
271
227
  }
@@ -13,8 +13,6 @@
13
13
  *
14
14
  * This enables proper pronunciation, emphasis, and pacing for TTS without
15
15
  * requiring authors to maintain separate catalog files.
16
- *
17
- * Part of PIE Assessment Toolkit.
18
16
  */
19
17
  import type { AccessibilityCatalog, ConfigEntity } from "@pie-players/pie-players-shared/types";
20
18
  /**
@@ -25,19 +23,11 @@ export interface ExtractionResult {
25
23
  cleanedConfig: ConfigEntity;
26
24
  }
27
25
  /**
28
- * Sanitize an SSML subtree in place: remove disallowed attributes and
29
- * replace disallowed elements with their text content (so phrasing order
30
- * is preserved but no scripts / iframes / unknown tags survive).
31
- */
32
- export declare function sanitizeSsmlElement(element: Element): void;
33
- /**
34
- * Serialize a sanitized `<speak>` element back to a minimal SSML string.
35
- * Uses a manual walker instead of `outerHTML` so that any namespace
36
- * quirks (e.g. `xmlns` attributes added by the HTML parser) are
37
- * normalised and disallowed descendants cannot reappear through quirks
38
- * in the serializer.
26
+ * SSML cut down to the allow-listed tags and attributes. Input with no markup,
27
+ * or with no DOMParser available, comes back unchanged. Otherwise the first
28
+ * <speak> element comes back sanitized, or, when there is none, the sanitized
29
+ * fragment with no <speak> wrapper.
39
30
  */
40
- export declare function serializeSsmlElement(element: Element): string;
41
31
  export declare function sanitizeSsmlString(input: string): string;
42
32
  /**
43
33
  * SSMLExtractor Service
@@ -46,26 +36,13 @@ export declare function sanitizeSsmlString(input: string): string;
46
36
  */
47
37
  export declare class SSMLExtractor {
48
38
  private catalogCounter;
49
- /**
50
- * Extract SSML from entire item config (markup + models)
51
- *
52
- * @param config Item configuration with potential embedded SSML
53
- * @returns Extracted catalogs and cleaned config
54
- */
39
+ /** Extract SSML from the item's markup, prompts and choice labels. */
55
40
  extractFromItemConfig(config: ConfigEntity): ExtractionResult;
56
- /**
57
- * Extract SSML from markup string
58
- *
59
- * @param markup HTML markup potentially containing <speak> elements
60
- * @param idPrefix Prefix for generating catalog IDs
61
- * @returns Extracted catalogs and cleaned markup
62
- */
41
+ /** Extract SSML from one markup string. */
63
42
  private extractFromMarkup;
64
43
  /**
65
- * Generate unique catalog ID
66
- *
67
- * @param prefix Context prefix (e.g., 'prompt-q1', 'choice-q1-a')
68
- * @returns Unique catalog ID (e.g., 'auto-prompt-q1-0')
44
+ * A catalog id unique to this extractor until reset(), e.g.
45
+ * `auto-prompt-q1-0`.
69
46
  */
70
47
  private generateCatalogId;
71
48
  /**
@@ -13,8 +13,6 @@
13
13
  *
14
14
  * This enables proper pronunciation, emphasis, and pacing for TTS without
15
15
  * requiring authors to maintain separate catalog files.
16
- *
17
- * Part of PIE Assessment Toolkit.
18
16
  */
19
17
  /**
20
18
  * Allow-listed SSML elements (from the SSML 1.1 spec subset we forward to
@@ -38,11 +36,6 @@ const SSML_ALLOWED_TAGS = new Set([
38
36
  "audio",
39
37
  "lexicon",
40
38
  ]);
41
- /**
42
- * Allow-listed attributes across SSML elements. Attribute allow-listing is
43
- * global (not per-tag) because the union is small and consistent across
44
- * providers.
45
- */
46
39
  /**
47
40
  * Tags whose content must be dropped entirely (rather than unwrapped to
48
41
  * preserve phrasing text). These are dangerous containers whose text
@@ -58,6 +51,10 @@ const SSML_DROP_FULLY_TAGS = new Set([
58
51
  "noscript",
59
52
  "template",
60
53
  ]);
54
+ /**
55
+ * Allow-listed attributes across SSML elements. One list covers every tag
56
+ * because the union is small and consistent across providers.
57
+ */
61
58
  const SSML_ALLOWED_ATTRS = new Set([
62
59
  "alphabet",
63
60
  "ph",
@@ -153,7 +150,7 @@ function isSafeSsmlAudioSrc(raw) {
153
150
  * replace disallowed elements with their text content (so phrasing order
154
151
  * is preserved but no scripts / iframes / unknown tags survive).
155
152
  */
156
- export function sanitizeSsmlElement(element) {
153
+ function sanitizeSsmlElement(element) {
157
154
  // Walk children snapshot first because we may replace nodes as we go.
158
155
  const children = Array.from(element.children);
159
156
  for (const child of children) {
@@ -175,10 +172,8 @@ export function sanitizeSsmlElement(element) {
175
172
  element.removeAttribute(attrName);
176
173
  }
177
174
  }
178
- // Belt-and-braces: even though `src` is allow-listed, SSML `<audio>`
179
- // sends its URL to a server-side TTS provider that will dereference
180
- // it. Refuse non-http(s) schemes and private / cloud-metadata hosts
181
- // at the SSML boundary so we never forward an SSRF primitive.
175
+ // `src` survives the attribute allow-list, and a server-side provider
176
+ // fetches it: keep it only for an http(s) URL off SSML_PRIVATE_AUDIO_HOSTS.
182
177
  if (element.tagName.toLowerCase() === "audio") {
183
178
  const src = element.getAttribute("src");
184
179
  if (src !== null && !isSafeSsmlAudioSrc(src)) {
@@ -193,7 +188,7 @@ export function sanitizeSsmlElement(element) {
193
188
  * normalised and disallowed descendants cannot reappear through quirks
194
189
  * in the serializer.
195
190
  */
196
- export function serializeSsmlElement(element) {
191
+ function serializeSsmlElement(element) {
197
192
  const tagName = element.tagName.toLowerCase();
198
193
  const attrs = Array.from(element.attributes)
199
194
  .filter((attr) => SSML_ALLOWED_ATTRS.has(attr.name.toLowerCase()))
@@ -227,6 +222,12 @@ export function serializeSsmlElement(element) {
227
222
  }
228
223
  return `<${tagName}${attrs}>${childPieces.join("")}</${tagName}>`;
229
224
  }
225
+ /**
226
+ * SSML cut down to the allow-listed tags and attributes. Input with no markup,
227
+ * or with no DOMParser available, comes back unchanged. Otherwise the first
228
+ * <speak> element comes back sanitized, or, when there is none, the sanitized
229
+ * fragment with no <speak> wrapper.
230
+ */
230
231
  export function sanitizeSsmlString(input) {
231
232
  const trimmed = String(input || "").trim();
232
233
  if (!trimmed || !trimmed.includes("<"))
@@ -281,12 +282,7 @@ function escapeXmlAttr(value) {
281
282
  */
282
283
  export class SSMLExtractor {
283
284
  catalogCounter = 0;
284
- /**
285
- * Extract SSML from entire item config (markup + models)
286
- *
287
- * @param config Item configuration with potential embedded SSML
288
- * @returns Extracted catalogs and cleaned config
289
- */
285
+ /** Extract SSML from the item's markup, prompts and choice labels. */
290
286
  extractFromItemConfig(config) {
291
287
  const allCatalogs = [];
292
288
  // Shallow clone of config + a fresh models array. The per-model/choice
@@ -336,18 +332,11 @@ export class SSMLExtractor {
336
332
  cleanedConfig,
337
333
  };
338
334
  }
339
- /**
340
- * Extract SSML from markup string
341
- *
342
- * @param markup HTML markup potentially containing <speak> elements
343
- * @param idPrefix Prefix for generating catalog IDs
344
- * @returns Extracted catalogs and cleaned markup
345
- */
335
+ /** Extract SSML from one markup string. */
346
336
  extractFromMarkup(markup, idPrefix) {
347
337
  if (!markup || markup.trim() === "") {
348
338
  return { catalogs: [], cleanedMarkup: markup };
349
339
  }
350
- // Check if running in browser environment
351
340
  if (typeof window === "undefined" || typeof DOMParser === "undefined") {
352
341
  console.warn("[SSMLExtractor] DOMParser not available (SSR?), skipping extraction");
353
342
  return { catalogs: [], cleanedMarkup: markup };
@@ -356,16 +345,12 @@ export class SSMLExtractor {
356
345
  try {
357
346
  const parser = new DOMParser();
358
347
  const doc = parser.parseFromString(expandSelfClosingVoidTags(markup), "text/html");
359
- // Find all <speak> elements
360
348
  const speakElements = Array.from(doc.querySelectorAll("speak"));
361
349
  if (speakElements.length === 0) {
362
- // No SSML found, return original markup
363
350
  return { catalogs: [], cleanedMarkup: markup };
364
351
  }
365
- // Process each <speak> element
366
352
  for (const speakEl of speakElements) {
367
353
  try {
368
- // Generate unique catalog ID
369
354
  const catalogId = this.generateCatalogId(idPrefix);
370
355
  // Sanitize before serializing so the SSML forwarded to the TTS
371
356
  // provider only contains spec-allowed tags/attributes. This
@@ -373,7 +358,6 @@ export class SSMLExtractor {
373
358
  // otherwise accept arbitrary HTML.
374
359
  sanitizeSsmlElement(speakEl);
375
360
  const ssmlContent = serializeSsmlElement(speakEl);
376
- // Extract language (check both xml:lang and lang attributes)
377
361
  const language = speakEl.getAttribute("xml:lang") ||
378
362
  speakEl.getAttribute("lang") ||
379
363
  "en-US";
@@ -409,7 +393,6 @@ export class SSMLExtractor {
409
393
  else {
410
394
  wrapper.setAttribute("data-catalog-idref", catalogId);
411
395
  }
412
- // Create catalog entry
413
396
  catalogs.push({
414
397
  identifier: catalogId,
415
398
  cards: [
@@ -423,24 +406,19 @@ export class SSMLExtractor {
423
406
  }
424
407
  catch (error) {
425
408
  console.error("[SSMLExtractor] Error processing <speak> element:", error);
426
- // Continue with other elements
427
409
  }
428
410
  }
429
- // Return cleaned markup
430
411
  const cleanedMarkup = doc.body.innerHTML;
431
412
  return { catalogs, cleanedMarkup };
432
413
  }
433
414
  catch (error) {
434
415
  console.error("[SSMLExtractor] Error parsing markup:", error);
435
- // Return original markup if parsing fails
436
416
  return { catalogs: [], cleanedMarkup: markup };
437
417
  }
438
418
  }
439
419
  /**
440
- * Generate unique catalog ID
441
- *
442
- * @param prefix Context prefix (e.g., 'prompt-q1', 'choice-q1-a')
443
- * @returns Unique catalog ID (e.g., 'auto-prompt-q1-0')
420
+ * A catalog id unique to this extractor until reset(), e.g.
421
+ * `auto-prompt-q1-0`.
444
422
  */
445
423
  generateCatalogId(prefix) {
446
424
  const id = `auto-${prefix}-${this.catalogCounter}`;
@@ -1,26 +1,28 @@
1
1
  /**
2
2
  * TTSService
3
3
  *
4
- * Text-to-speech service with pluggable provider architecture.
5
- * Manages playback state, coordinates multiple TTS entry points,
6
- * and integrates with HighlightCoordinator for word highlighting.
4
+ * Owns one read-aloud session: the installed provider, playback state, and the
5
+ * word and sentence highlights painted through HighlightCoordinator.
7
6
  *
8
- * Features:
9
- * - Pluggable TTS providers (browser, AWS Polly, etc.)
10
- * - QTI 3.0 accessibility catalogs (pre-authored spoken content)
11
- * - Unified playback state management
12
- * - Prevents conflicts from simultaneous speech
13
- * - Word highlighting integration
14
- * - Playback controls: play, pause, resume, stop
15
- *
16
- * Part of PIE Assessment Toolkit.
7
+ * Layout:
8
+ * - Provider and host wiring: initialize, setCatalogResolver, the highlight
9
+ * target resolver and math-speech sources.
10
+ * - Content resolution: catalog spoken card, then catalog-composed chunks
11
+ * (script and/or recording with a plain fallback), then generated speech.
12
+ * - Speech plan: structural pauses, seek segments, sentence segments and the
13
+ * normalized-to-DOM position map.
14
+ * - Playback: speakContent → executeSpeakPlayback, the run guard (speakRunId,
15
+ * runOwner), the pause hold and the playback-start barrier.
16
+ * - Highlight painting: boundary handling, the host resolver remap, catalog
17
+ * highlight plans.
18
+ * - Controls and state: pause/resume/stop/seek/rate, setState and listeners.
17
19
  */
18
- import type { ITTSProvider, TTSConfig, TTSProviderCapabilities } from "@pie-players/pie-tts";
20
+ import type { ITTSProvider, TTSConfig } from "@pie-players/pie-tts";
19
21
  import type { AccessibilityCatalogResolver } from "./AccessibilityCatalogResolver.js";
20
22
  import type { HighlightCoordinatorApi, SpeakOptions } from "./interfaces.js";
21
23
  import type { TTSHighlightTargetResolverProvider } from "./tts/highlight-target-resolver.js";
22
24
  import { type SREMathSpeechOptions } from "./tts/math-speech.js";
23
- export type { ITTSProvider, ITTSProviderImplementation, TTSConfig, TTSProviderCapabilities, } from "@pie-players/pie-tts";
25
+ import type { ToolkitTTSConfig } from "./tts/provider-options.js";
24
26
  /**
25
27
  * Playback state
26
28
  */
@@ -32,8 +34,6 @@ export declare enum PlaybackState {
32
34
  ERROR = "error"
33
35
  }
34
36
  /**
35
- * TTSService
36
- *
37
37
  * Instantiable service for text-to-speech functionality.
38
38
  * Each instance manages its own playback state.
39
39
  */
@@ -57,6 +57,7 @@ export declare class TTSService {
57
57
  private listeners;
58
58
  private lastError;
59
59
  private speakRunId;
60
+ private runOwner;
60
61
  private currentBoundaryOffset;
61
62
  private activeWordBoundaryOffset;
62
63
  private seekSegments;
@@ -74,12 +75,24 @@ export declare class TTSService {
74
75
  private pendingPlaybackRateRequest;
75
76
  private activePlaybackRate;
76
77
  private activeHighlightMode;
78
+ private wordlessPartHighlight;
77
79
  private lastRenderedRegionTarget;
78
80
  private telemetryReporter;
79
81
  private readonly generatedMathSpeechResolver;
80
- constructor();
81
82
  private clearPlaybackStartBarrier;
82
83
  private notifyPlaybackStarted;
84
+ /**
85
+ * A word-mode part whose playback sends no boundaries, such as a server
86
+ * response without speech marks, highlights as a sentence-mode read does.
87
+ * Catalog chunks and a single speak highlight the same in both modes.
88
+ */
89
+ private highlightWordlessPlayback;
90
+ /**
91
+ * Defers marking the run playing, and the highlights queued through
92
+ * runWhenPlaybackStarts, until the provider or the recording signals that
93
+ * audio started. Returns the barrier's cleanup, or null when nothing will
94
+ * signal a start, in which case the caller marks the run playing itself.
95
+ */
83
96
  private installPlaybackStartBarrier;
84
97
  private runWhenPlaybackStarts;
85
98
  /** Resolves when run `runId` may start its next part: at once unless paused. */
@@ -105,11 +118,8 @@ export declare class TTSService {
105
118
  private emitTelemetry;
106
119
  /**
107
120
  * Initialize TTS service with a provider
108
- *
109
- * @param provider TTS provider instance
110
- * @param config Provider configuration
111
121
  */
112
- initialize(provider: ITTSProvider, config?: Partial<TTSConfig>): Promise<void>;
122
+ initialize(provider: ITTSProvider, config?: Partial<ToolkitTTSConfig>): Promise<void>;
113
123
  /**
114
124
  * Update rate, pitch, voice or provider options without reinitializing.
115
125
  * `providerOptions` merge over the ones already configured.
@@ -153,8 +163,6 @@ export declare class TTSService {
153
163
  *
154
164
  * When set, the speak() method will check for pre-authored spoken
155
165
  * content in catalogs before falling back to generated TTS.
156
- *
157
- * @param resolver AccessibilityCatalogResolver instance
158
166
  */
159
167
  setCatalogResolver(resolver: AccessibilityCatalogResolver): void;
160
168
  private getHighlightResolverRuntime;
@@ -171,10 +179,6 @@ export declare class TTSService {
171
179
  */
172
180
  private paintTTSWord;
173
181
  private paintTTSSentenceRanges;
174
- /**
175
- * Get provider capabilities
176
- */
177
- getCapabilities(): TTSProviderCapabilities | null;
178
182
  private providerOptions;
179
183
  /** The longest text the provider accepts in one speak, when it declares one. */
180
184
  private maxTextLength;
@@ -187,8 +191,8 @@ export declare class TTSService {
187
191
  /** Segments no longer than the provider's `maxTextLength`, offsets kept. */
188
192
  private segmentsWithinLimit;
189
193
  /**
190
- * Build a character-by-character map from normalized text positions to DOM positions
191
- * This handles the complexity of whitespace normalization
194
+ * Text-collection options for `locale`, with the provider's boundary-spacing
195
+ * mode when it names a known one.
192
196
  */
193
197
  private getTextProcessingOptions;
194
198
  /**
@@ -203,12 +207,15 @@ export declare class TTSService {
203
207
  * this service was initialized with.
204
208
  */
205
209
  getMathSpeechOptions(): SREMathSpeechOptions | undefined;
210
+ /**
211
+ * Rebuilds normalizedToDOM from `element`'s visible text; traced runs also
212
+ * log how it differs from `spokenText`.
213
+ */
206
214
  private buildPositionMap;
207
215
  private getStructuralPauseProfile;
208
216
  private resolvePauseMsFromUnits;
209
217
  private resolveHighlightMode;
210
218
  private hasExplicitBreakSemantics;
211
- private isElementHidden;
212
219
  private getBoundaryStrength;
213
220
  private getBoundaryAnchor;
214
221
  /**
@@ -246,12 +253,19 @@ export declare class TTSService {
246
253
  * running continues.
247
254
  */
248
255
  speak(target: Range | Element, options?: SpeakOptions): Promise<void>;
256
+ /**
257
+ * Sets the rate a run starts at, after any rate write in flight. The run has
258
+ * no playback yet, so nothing restarts.
259
+ */
260
+ private applyRunRate;
249
261
  /**
250
262
  * Suppression is read from the live ancestors of what is read, because the
251
263
  * element carrying it may sit above the content root.
252
264
  */
253
265
  private isSuppressedTarget;
254
266
  private resolveElementTarget;
267
+ /** Whether the root's card, or a card of a node inside it, would be read. */
268
+ private holdsSpokenCard;
255
269
  private resolveRangeTarget;
256
270
  private speakContent;
257
271
  /**
@@ -327,7 +341,8 @@ export declare class TTSService {
327
341
  seekForward(units?: number): Promise<void>;
328
342
  seekBackward(units?: number): Promise<void>;
329
343
  /**
330
- * Stop playback
344
+ * Stop playback and release the run owner. A run that ends on its own keeps
345
+ * its owner, so the owning control can replay it.
331
346
  */
332
347
  stop(): void;
333
348
  /**
@@ -340,41 +355,19 @@ export declare class TTSService {
340
355
  * A disposed service cannot speak or be initialized again.
341
356
  */
342
357
  dispose(): void;
343
- /**
344
- * Request UI-level TTS controls to hand off/deactivate.
345
- *
346
- * This is intentionally separate from playback controls so hosts can orchestrate
347
- * control handoff explicitly (for example: stop playback, then dismiss controls).
348
- */
349
- requestControlHandoff(): void;
350
- /**
351
- * Check if currently playing
352
- */
353
- isPlaying(): boolean;
354
- /**
355
- * Check if paused
356
- */
357
- isPaused(): boolean;
358
358
  /**
359
359
  * Get current state
360
360
  */
361
361
  getState(): PlaybackState;
362
- /**
363
- * Get current text being spoken
364
- */
365
- getCurrentText(): string | null;
366
- /**
367
- * Get the last error message
368
- * Returns null if no error has occurred
369
- */
370
- getLastError(): string | null;
362
+ getRunOwner(): string | null;
371
363
  /**
372
364
  * Subscribe to playback state changes. The returned function unsubscribes;
373
365
  * each subscription is its own, so the same callback can be subscribed twice.
374
366
  */
375
367
  onStateChange(callback: (state: PlaybackState) => void): () => void;
376
368
  /**
377
- * Set state and notify listeners
369
+ * Set state and notify listeners. `announceUnchanged` notifies listeners even
370
+ * when the state does not change; telemetry fires only on a change.
378
371
  */
379
372
  private setState;
380
373
  }