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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +54 -69
  2. package/dist/components/ItemToolBar.custom-element.js +479 -324
  3. package/dist/components/PieAssessmentToolkit.custom-element.js +19498 -3590
  4. package/dist/components/SectionToolBar.custom-element.js +761 -507
  5. package/dist/index.d.ts +3 -6
  6. package/dist/index.js +1 -3
  7. package/dist/index.js.map +1 -1
  8. package/dist/policy/core/ToolPolicyEngine.d.ts +1 -2
  9. package/dist/policy/core/ToolPolicyEngine.js +1 -2
  10. package/dist/policy/core/ToolPolicyEngine.js.map +1 -1
  11. package/dist/policy/core/pnp-policy-inputs.d.ts +2 -3
  12. package/dist/policy/core/pnp-policy-inputs.js +2 -3
  13. package/dist/policy/core/pnp-policy-inputs.js.map +1 -1
  14. package/dist/policy/core/provenance.d.ts +7 -34
  15. package/dist/policy/core/provenance.js +4 -21
  16. package/dist/policy/core/provenance.js.map +1 -1
  17. package/dist/policy/sources/PnpPolicySource.d.ts +6 -19
  18. package/dist/policy/sources/PnpPolicySource.js +6 -19
  19. package/dist/policy/sources/PnpPolicySource.js.map +1 -1
  20. package/dist/runtime/SectionRuntimeEngine.d.ts +13 -28
  21. package/dist/runtime/SectionRuntimeEngine.js +13 -27
  22. package/dist/runtime/SectionRuntimeEngine.js.map +1 -1
  23. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +2 -2
  24. package/dist/runtime/adapter/SectionEngineAdapter.js +2 -2
  25. package/dist/runtime/adapter/SectionEngineAdapter.js.map +1 -1
  26. package/dist/runtime/adapter/coordinator-bridge.d.ts +1 -1
  27. package/dist/runtime/adapter/coordinator-bridge.js +1 -1
  28. package/dist/runtime/adapter/coordinator-bridge.js.map +1 -1
  29. package/dist/runtime/adapter/dom-event-bridge.d.ts +1 -1
  30. package/dist/runtime/adapter/dom-event-bridge.js +1 -1
  31. package/dist/runtime/adapter/dom-event-bridge.js.map +1 -1
  32. package/dist/runtime/catalog-registration.d.ts +12 -0
  33. package/dist/runtime/catalog-registration.js +54 -0
  34. package/dist/runtime/catalog-registration.js.map +1 -0
  35. package/dist/runtime/core/engine-output.d.ts +2 -2
  36. package/dist/runtime/core/engine-output.js +2 -2
  37. package/dist/runtime/core/engine-output.js.map +1 -1
  38. package/dist/runtime/core/engine-readiness.d.ts +2 -2
  39. package/dist/runtime/core/engine-readiness.js.map +1 -1
  40. package/dist/runtime/core/engine-resolver.d.ts +13 -41
  41. package/dist/runtime/core/engine-resolver.js +19 -76
  42. package/dist/runtime/core/engine-resolver.js.map +1 -1
  43. package/dist/runtime/core/engine-transition.d.ts +1 -1
  44. package/dist/runtime/core/engine-transition.js +1 -1
  45. package/dist/runtime/core/engine-transition.js.map +1 -1
  46. package/dist/runtime/engine.d.ts +1 -1
  47. package/dist/runtime/engine.js.map +1 -1
  48. package/dist/runtime/section-runtime-engine-host-context.d.ts +22 -15
  49. package/dist/runtime/section-runtime-engine-host-context.js +10 -10
  50. package/dist/runtime/section-runtime-engine-host-context.js.map +1 -1
  51. package/dist/runtime/stage-emit-gate.d.ts +4 -4
  52. package/dist/runtime/stage-emit-gate.js +3 -3
  53. package/dist/runtime/stage-emit-gate.js.map +1 -1
  54. package/dist/services/AccessibilityCatalogResolver.d.ts +23 -0
  55. package/dist/services/AccessibilityCatalogResolver.js +200 -28
  56. package/dist/services/AccessibilityCatalogResolver.js.map +1 -1
  57. package/dist/services/HighlightCoordinator.d.ts +25 -0
  58. package/dist/services/HighlightCoordinator.js +125 -0
  59. package/dist/services/HighlightCoordinator.js.map +1 -1
  60. package/dist/services/SSMLExtractor.d.ts +16 -1
  61. package/dist/services/SSMLExtractor.js +86 -7
  62. package/dist/services/SSMLExtractor.js.map +1 -1
  63. package/dist/services/TTSService.d.ts +30 -4
  64. package/dist/services/TTSService.js +652 -159
  65. package/dist/services/TTSService.js.map +1 -1
  66. package/dist/services/ToolRegistry.d.ts +2 -2
  67. package/dist/services/ToolRegistry.js +1 -1
  68. package/dist/services/ToolRegistry.js.map +1 -1
  69. package/dist/services/ToolkitCoordinator.d.ts +16 -73
  70. package/dist/services/ToolkitCoordinator.js +34 -113
  71. package/dist/services/ToolkitCoordinator.js.map +1 -1
  72. package/dist/services/interfaces.d.ts +20 -2
  73. package/dist/services/interfaces.js.map +1 -1
  74. package/dist/services/tool-config-validation.d.ts +1 -1
  75. package/dist/services/tool-config-validation.js +7 -7
  76. package/dist/services/tool-config-validation.js.map +1 -1
  77. package/dist/services/tool-instance-id.js +1 -1
  78. package/dist/services/tool-instance-id.js.map +1 -1
  79. package/dist/services/tool-providers/TTSToolProvider.js +3 -3
  80. package/dist/services/tool-providers/TTSToolProvider.js.map +1 -1
  81. package/dist/services/tools-config-normalizer.d.ts +13 -2
  82. package/dist/services/tools-config-normalizer.js +3 -8
  83. package/dist/services/tools-config-normalizer.js.map +1 -1
  84. package/dist/services/tts/browser-provider.js +2 -0
  85. package/dist/services/tts/browser-provider.js.map +1 -1
  86. package/dist/services/tts/catalog-span-alignment.d.ts +39 -0
  87. package/dist/services/tts/catalog-span-alignment.js +300 -0
  88. package/dist/services/tts/catalog-span-alignment.js.map +1 -0
  89. package/dist/services/tts/generated-speech/assemble-plan.d.ts +33 -0
  90. package/dist/services/tts/generated-speech/assemble-plan.js +111 -0
  91. package/dist/services/tts/generated-speech/assemble-plan.js.map +1 -0
  92. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +25 -0
  93. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +66 -0
  94. package/dist/services/tts/generated-speech/dom/build-dom-plan.js.map +1 -0
  95. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +19 -0
  96. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.js +71 -0
  97. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.js.map +1 -0
  98. package/dist/services/tts/generated-speech/dom/types.d.ts +37 -0
  99. package/dist/services/tts/generated-speech/dom/types.js +2 -0
  100. package/dist/services/tts/generated-speech/dom/types.js.map +1 -0
  101. package/dist/services/tts/generated-speech/index.d.ts +17 -0
  102. package/dist/services/tts/generated-speech/index.js +17 -0
  103. package/dist/services/tts/generated-speech/index.js.map +1 -0
  104. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +15 -0
  105. package/dist/services/tts/generated-speech/math-speech-cache.js +78 -0
  106. package/dist/services/tts/generated-speech/math-speech-cache.js.map +1 -0
  107. package/dist/services/tts/generated-speech/types.d.ts +104 -0
  108. package/dist/services/tts/generated-speech/types.js +18 -0
  109. package/dist/services/tts/generated-speech/types.js.map +1 -0
  110. package/dist/services/tts/highlight-pipeline/boundary-normalizer.d.ts +2 -0
  111. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +106 -0
  112. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js.map +1 -0
  113. package/dist/services/tts/highlight-pipeline/create-highlight-plan.d.ts +6 -0
  114. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +217 -0
  115. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js.map +1 -0
  116. package/dist/services/tts/highlight-pipeline/highlight-policy.d.ts +2 -0
  117. package/dist/services/tts/highlight-pipeline/highlight-policy.js +32 -0
  118. package/dist/services/tts/highlight-pipeline/highlight-policy.js.map +1 -0
  119. package/dist/services/tts/highlight-pipeline/index.d.ts +9 -0
  120. package/dist/services/tts/highlight-pipeline/index.js +9 -0
  121. package/dist/services/tts/highlight-pipeline/index.js.map +1 -0
  122. package/dist/services/tts/highlight-pipeline/math-semantic-resolver.d.ts +3 -0
  123. package/dist/services/tts/highlight-pipeline/math-semantic-resolver.js +32 -0
  124. package/dist/services/tts/highlight-pipeline/math-semantic-resolver.js.map +1 -0
  125. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.d.ts +29 -0
  126. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +44 -0
  127. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js.map +1 -0
  128. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.d.ts +2 -0
  129. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +20 -0
  130. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js.map +1 -0
  131. package/dist/services/tts/highlight-pipeline/readable-region.d.ts +1 -0
  132. package/dist/services/tts/highlight-pipeline/readable-region.js +19 -0
  133. package/dist/services/tts/highlight-pipeline/readable-region.js.map +1 -0
  134. package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.d.ts +6 -0
  135. package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.js +198 -0
  136. package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.js.map +1 -0
  137. package/dist/services/tts/highlight-pipeline/types.d.ts +72 -0
  138. package/dist/services/tts/highlight-pipeline/types.js +2 -0
  139. package/dist/services/tts/highlight-pipeline/types.js.map +1 -0
  140. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +2 -0
  141. package/dist/services/tts/highlight-pipeline/visible-map-range.js +22 -0
  142. package/dist/services/tts/highlight-pipeline/visible-map-range.js.map +1 -0
  143. package/dist/services/tts/math-alignment/index.d.ts +30 -0
  144. package/dist/services/tts/math-alignment/index.js +66 -0
  145. package/dist/services/tts/math-alignment/index.js.map +1 -0
  146. package/dist/services/tts/math-alignment/mathml-tokenizer.d.ts +2 -0
  147. package/dist/services/tts/math-alignment/mathml-tokenizer.js +317 -0
  148. package/dist/services/tts/math-alignment/mathml-tokenizer.js.map +1 -0
  149. package/dist/services/tts/math-alignment/range-resolver.d.ts +10 -0
  150. package/dist/services/tts/math-alignment/range-resolver.js +13 -0
  151. package/dist/services/tts/math-alignment/range-resolver.js.map +1 -0
  152. package/dist/services/tts/math-alignment/sequence-aligner.d.ts +7 -0
  153. package/dist/services/tts/math-alignment/sequence-aligner.js +119 -0
  154. package/dist/services/tts/math-alignment/sequence-aligner.js.map +1 -0
  155. package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +31 -0
  156. package/dist/services/tts/math-alignment/speech-tokenizer.js +143 -0
  157. package/dist/services/tts/math-alignment/speech-tokenizer.js.map +1 -0
  158. package/dist/services/tts/math-alignment/types.d.ts +58 -0
  159. package/dist/services/tts/math-alignment/types.js +2 -0
  160. package/dist/services/tts/math-alignment/types.js.map +1 -0
  161. package/dist/services/tts/math-aware-text-processing.d.ts +18 -0
  162. package/dist/services/tts/math-aware-text-processing.js +348 -0
  163. package/dist/services/tts/math-aware-text-processing.js.map +1 -0
  164. package/dist/services/tts/math-speech.d.ts +44 -0
  165. package/dist/services/tts/math-speech.js +147 -0
  166. package/dist/services/tts/math-speech.js.map +1 -0
  167. package/dist/services/tts/mathml-sanitization.d.ts +1 -0
  168. package/dist/services/tts/mathml-sanitization.js +170 -0
  169. package/dist/services/tts/mathml-sanitization.js.map +1 -0
  170. package/dist/services/tts/ssml/spoken-text.d.ts +33 -0
  171. package/dist/services/tts/ssml/spoken-text.js +159 -0
  172. package/dist/services/tts/ssml/spoken-text.js.map +1 -0
  173. package/dist/services/tts/text-processing.d.ts +3 -0
  174. package/dist/services/tts/text-processing.js +30 -4
  175. package/dist/services/tts/text-processing.js.map +1 -1
  176. package/dist/services/tts-runtime-config.d.ts +17 -1
  177. package/dist/services/tts-runtime-config.js +11 -1
  178. package/dist/services/tts-runtime-config.js.map +1 -1
  179. package/dist/tools/registrations/accessibility-tools.js +0 -1
  180. package/dist/tools/registrations/accessibility-tools.js.map +1 -1
  181. package/dist/tools/registrations/calculator.js +41 -2
  182. package/dist/tools/registrations/calculator.js.map +1 -1
  183. package/dist/tools/tool-tag-map.js +0 -1
  184. package/dist/tools/tool-tag-map.js.map +1 -1
  185. package/package.json +8 -11
  186. package/dist/services/PNPToolResolver.d.ts +0 -280
  187. package/dist/services/PNPToolResolver.js +0 -514
  188. package/dist/services/PNPToolResolver.js.map +0 -1
  189. package/dist/services/pnp-provenance.d.ts +0 -166
  190. package/dist/services/pnp-provenance.js +0 -231
  191. package/dist/services/pnp-provenance.js.map +0 -1
@@ -0,0 +1,44 @@
1
+ import type { MathAwareSpeechChunk } from "./math-aware-text-processing.js";
2
+ interface SpeechRuleEngineApi {
3
+ setupEngine?: (options: Record<string, unknown>) => Promise<void> | void;
4
+ engineReady?: () => Promise<void>;
5
+ toSpeech: (mathml: string) => string;
6
+ }
7
+ export interface ResolveMathSpeechOptions {
8
+ language?: string;
9
+ loadSre?: () => Promise<SpeechRuleEngineApi>;
10
+ /**
11
+ * Optional SRE configuration supplied by the host. This deliberately exposes
12
+ * SRE's own knobs instead of adding toolkit speech rewrites.
13
+ */
14
+ mathSpeech?: SREMathSpeechOptions;
15
+ /**
16
+ * Also produce SRE's SSML rendering (markup: "ssml") for the equation, for
17
+ * the runtime generated-SSML path (PIE-623). Only populated when the input
18
+ * is a single math chunk and SRE succeeds. The plain `speechText` is always
19
+ * computed with markup: "none" so the plain path is unaffected.
20
+ */
21
+ produceSsml?: boolean;
22
+ }
23
+ export interface SREMathSpeechOptions {
24
+ /** SRE domain, e.g. "clearspeak" or "mathspeak". Defaults by locale. */
25
+ domain?: string;
26
+ /** Raw SRE style/preference string. ClearSpeak preferences are ":"-joined. */
27
+ style?: string;
28
+ /** Extra SRE setupEngine options for hosts that need a newly exposed SRE knob. */
29
+ engineOptions?: Record<string, unknown>;
30
+ }
31
+ export interface ResolvedMathSpeech {
32
+ speechText: string;
33
+ usedMathSpeech: boolean;
34
+ usedFallback: boolean;
35
+ /**
36
+ * SRE SSML rendering of the equation (a self-contained `<speak>` document),
37
+ * present only when `produceSsml` was requested for a single math chunk and
38
+ * SRE produced trackable SSML.
39
+ */
40
+ ssml?: string;
41
+ }
42
+ export declare const normalizeSREMathSpeechOptions: (options: unknown) => SREMathSpeechOptions | undefined;
43
+ export declare const resolveMathSpeechFromChunks: (chunks: MathAwareSpeechChunk[], options?: ResolveMathSpeechOptions) => Promise<ResolvedMathSpeech>;
44
+ export {};
@@ -0,0 +1,147 @@
1
+ import { normalizeTextForSpeech } from "./text-processing.js";
2
+ let sreLoadPromise = null;
3
+ let sreOperationQueue = Promise.resolve();
4
+ const normalizeLocale = (language) => (language || "en").split("-")[0].toLowerCase() || "en";
5
+ const defaultLoadSre = async () => {
6
+ if (!sreLoadPromise) {
7
+ sreLoadPromise = import("speech-rule-engine").then((module) => {
8
+ const candidate = module.default ||
9
+ module;
10
+ if (!candidate || typeof candidate.toSpeech !== "function") {
11
+ throw new Error("speech-rule-engine did not expose toSpeech");
12
+ }
13
+ return candidate;
14
+ });
15
+ }
16
+ return sreLoadPromise;
17
+ };
18
+ // SRE's ClearSpeak rule set is English-only; MathSpeak is localized to many more
19
+ // locales. Use ClearSpeak for English (highest-quality math prose) and MathSpeak
20
+ // elsewhere, so non-English content gets locale-appropriate voicing instead of
21
+ // silently falling back to English ClearSpeak rules.
22
+ const domainForLocale = (locale) => locale === "en" ? "clearspeak" : "mathspeak";
23
+ const isPlainRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
24
+ const trimOption = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
25
+ export const normalizeSREMathSpeechOptions = (options) => {
26
+ if (!isPlainRecord(options))
27
+ return undefined;
28
+ const domain = trimOption(options.domain);
29
+ const style = trimOption(options.style);
30
+ const engineOptions = isPlainRecord(options.engineOptions)
31
+ ? options.engineOptions
32
+ : undefined;
33
+ if (!domain && !style && !engineOptions)
34
+ return undefined;
35
+ return {
36
+ ...(domain ? { domain } : {}),
37
+ ...(style ? { style } : {}),
38
+ ...(engineOptions ? { engineOptions } : {}),
39
+ };
40
+ };
41
+ const setupSre = async (sre, language, markup, mathSpeech) => {
42
+ const locale = normalizeLocale(language);
43
+ const normalizedMathSpeech = normalizeSREMathSpeechOptions(mathSpeech);
44
+ await sre.setupEngine?.({
45
+ ...(normalizedMathSpeech?.engineOptions || {}),
46
+ locale,
47
+ domain: normalizedMathSpeech?.domain || domainForLocale(locale),
48
+ ...(normalizedMathSpeech?.style ? { style: normalizedMathSpeech.style } : {}),
49
+ modality: "speech",
50
+ markup,
51
+ });
52
+ await sre.engineReady?.();
53
+ };
54
+ const runSerializedSreOperation = async (operation) => {
55
+ const result = sreOperationQueue.then(operation, operation);
56
+ sreOperationQueue = result.then(() => undefined, () => undefined);
57
+ return result;
58
+ };
59
+ const toSpeechWithSreSettings = async (sre, mathml, language, markup, mathSpeech) => runSerializedSreOperation(async () => {
60
+ await setupSre(sre, language, markup, mathSpeech);
61
+ return sre.toSpeech(mathml);
62
+ });
63
+ // SRE's SSML rendering wraps the spoken words in highlight-safe structural tags
64
+ // (`speak`, `prosody`, `say-as interpret-as="character"`, `break`), all handled
65
+ // by `extractSpokenText`, so word-boundary offsets into the raw SSML still map
66
+ // back to spoken tokens. SRE also prepends an XML prolog (`<?xml …?>`); AWS
67
+ // Polly and Google Cloud TTS reject SSML that does not begin with `<speak>`, so
68
+ // we strip the prolog here. The stripped string is the single source of truth:
69
+ // it is both what the provider receives and what the alignment tokenizes, so
70
+ // offsets stay consistent. We only accept output that contains a `<speak>`
71
+ // element, so a bad/empty render degrades to plain text.
72
+ const stripXmlProlog = (ssml) => ssml.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
73
+ const generateMathSsml = async (sre, mathml, language, mathSpeech) => {
74
+ try {
75
+ const ssml = stripXmlProlog(await toSpeechWithSreSettings(sre, mathml, language, "ssml", mathSpeech));
76
+ return ssml.includes("<speak") ? ssml : null;
77
+ }
78
+ catch (error) {
79
+ console.debug("[TTSService] Math SSML generation failed; using plain", {
80
+ message: error instanceof Error ? error.message : String(error),
81
+ });
82
+ return null;
83
+ }
84
+ };
85
+ export const resolveMathSpeechFromChunks = async (chunks, options = {}) => {
86
+ if (!chunks.some((chunk) => chunk.type === "math")) {
87
+ return {
88
+ speechText: normalizeTextForSpeech(chunks
89
+ .map((chunk) => chunk.type === "text" ? chunk.text : chunk.fallbackText)
90
+ .join(" ")),
91
+ usedMathSpeech: false,
92
+ usedFallback: false,
93
+ };
94
+ }
95
+ let sre = null;
96
+ let usedMathSpeech = false;
97
+ let usedFallback = false;
98
+ const loadSre = options.loadSre || defaultLoadSre;
99
+ const speechParts = [];
100
+ for (const chunk of chunks) {
101
+ if (chunk.type === "text") {
102
+ speechParts.push(chunk.text);
103
+ continue;
104
+ }
105
+ try {
106
+ if (!sre) {
107
+ sre = await loadSre();
108
+ }
109
+ const speech = normalizeTextForSpeech(await toSpeechWithSreSettings(sre, chunk.mathml, options.language, "none", options.mathSpeech));
110
+ if (speech) {
111
+ speechParts.push(speech);
112
+ usedMathSpeech = true;
113
+ continue;
114
+ }
115
+ usedFallback = true;
116
+ speechParts.push(chunk.fallbackText);
117
+ }
118
+ catch (error) {
119
+ usedFallback = true;
120
+ console.debug("[TTSService] Math speech generation failed; using visible fallback", {
121
+ message: error instanceof Error ? error.message : String(error),
122
+ });
123
+ speechParts.push(chunk.fallbackText);
124
+ }
125
+ }
126
+ // Generated-SSML path: only meaningful for a single math chunk (the runtime
127
+ // composes one chunk per equation). Computed after the plain pass so the
128
+ // markup: "none" `speechText` above is unchanged. The "none" → "ssml"
129
+ // engine reconfiguration happens sequentially within this call.
130
+ const speechText = normalizeTextForSpeech(speechParts.join(" "));
131
+ let ssml;
132
+ if (options.produceSsml &&
133
+ sre &&
134
+ usedMathSpeech &&
135
+ chunks.length === 1 &&
136
+ chunks[0].type === "math") {
137
+ const generated = await generateMathSsml(sre, chunks[0].mathml, options.language, options.mathSpeech);
138
+ ssml = generated ?? undefined;
139
+ }
140
+ return {
141
+ speechText,
142
+ usedMathSpeech,
143
+ usedFallback,
144
+ ssml,
145
+ };
146
+ };
147
+ //# sourceMappingURL=math-speech.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"math-speech.js","sourceRoot":"","sources":["../../../src/services/tts/math-speech.ts"],"names":[],"mappings":"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","sourcesContent":["import type { MathAwareSpeechChunk } from \"./math-aware-text-processing.js\";\nimport { normalizeTextForSpeech } from \"./text-processing.js\";\n\ninterface SpeechRuleEngineApi {\n\tsetupEngine?: (options: Record<string, unknown>) => Promise<void> | void;\n\tengineReady?: () => Promise<void>;\n\ttoSpeech: (mathml: string) => string;\n}\n\nexport interface ResolveMathSpeechOptions {\n\tlanguage?: string;\n\tloadSre?: () => Promise<SpeechRuleEngineApi>;\n\t/**\n\t * Optional SRE configuration supplied by the host. This deliberately exposes\n\t * SRE's own knobs instead of adding toolkit speech rewrites.\n\t */\n\tmathSpeech?: SREMathSpeechOptions;\n\t/**\n\t * Also produce SRE's SSML rendering (markup: \"ssml\") for the equation, for\n\t * the runtime generated-SSML path (PIE-623). Only populated when the input\n\t * is a single math chunk and SRE succeeds. The plain `speechText` is always\n\t * computed with markup: \"none\" so the plain path is unaffected.\n\t */\n\tproduceSsml?: boolean;\n}\n\nexport interface SREMathSpeechOptions {\n\t/** SRE domain, e.g. \"clearspeak\" or \"mathspeak\". Defaults by locale. */\n\tdomain?: string;\n\t/** Raw SRE style/preference string. ClearSpeak preferences are \":\"-joined. */\n\tstyle?: string;\n\t/** Extra SRE setupEngine options for hosts that need a newly exposed SRE knob. */\n\tengineOptions?: Record<string, unknown>;\n}\n\nexport interface ResolvedMathSpeech {\n\tspeechText: string;\n\tusedMathSpeech: boolean;\n\tusedFallback: boolean;\n\t/**\n\t * SRE SSML rendering of the equation (a self-contained `<speak>` document),\n\t * present only when `produceSsml` was requested for a single math chunk and\n\t * SRE produced trackable SSML.\n\t */\n\tssml?: string;\n}\n\nlet sreLoadPromise: Promise<SpeechRuleEngineApi> | null = null;\nlet sreOperationQueue: Promise<unknown> = Promise.resolve();\n\nconst normalizeLocale = (language?: string): string =>\n\t(language || \"en\").split(\"-\")[0].toLowerCase() || \"en\";\n\nconst defaultLoadSre = async (): Promise<SpeechRuleEngineApi> => {\n\tif (!sreLoadPromise) {\n\t\tsreLoadPromise = import(\"speech-rule-engine\").then((module) => {\n\t\t\tconst candidate =\n\t\t\t\t(module as { default?: SpeechRuleEngineApi }).default ||\n\t\t\t\t(module as unknown as SpeechRuleEngineApi);\n\t\t\tif (!candidate || typeof candidate.toSpeech !== \"function\") {\n\t\t\t\tthrow new Error(\"speech-rule-engine did not expose toSpeech\");\n\t\t\t}\n\t\t\treturn candidate;\n\t\t});\n\t}\n\treturn sreLoadPromise;\n};\n\n// SRE's ClearSpeak rule set is English-only; MathSpeak is localized to many more\n// locales. Use ClearSpeak for English (highest-quality math prose) and MathSpeak\n// elsewhere, so non-English content gets locale-appropriate voicing instead of\n// silently falling back to English ClearSpeak rules.\nconst domainForLocale = (locale: string): string =>\n\tlocale === \"en\" ? \"clearspeak\" : \"mathspeak\";\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value);\n\nconst trimOption = (value: unknown): string | undefined =>\n\ttypeof value === \"string\" && value.trim() ? value.trim() : undefined;\n\nexport const normalizeSREMathSpeechOptions = (\n\toptions: unknown,\n): SREMathSpeechOptions | undefined => {\n\tif (!isPlainRecord(options)) return undefined;\n\tconst domain = trimOption(options.domain);\n\tconst style = trimOption(options.style);\n\tconst engineOptions = isPlainRecord(options.engineOptions)\n\t\t? options.engineOptions\n\t\t: undefined;\n\tif (!domain && !style && !engineOptions) return undefined;\n\treturn {\n\t\t...(domain ? { domain } : {}),\n\t\t...(style ? { style } : {}),\n\t\t...(engineOptions ? { engineOptions } : {}),\n\t};\n};\n\nconst setupSre = async (\n\tsre: SpeechRuleEngineApi,\n\tlanguage: string | undefined,\n\tmarkup: \"none\" | \"ssml\",\n\tmathSpeech?: SREMathSpeechOptions,\n): Promise<void> => {\n\tconst locale = normalizeLocale(language);\n\tconst normalizedMathSpeech = normalizeSREMathSpeechOptions(mathSpeech);\n\tawait sre.setupEngine?.({\n\t\t...(normalizedMathSpeech?.engineOptions || {}),\n\t\tlocale,\n\t\tdomain: normalizedMathSpeech?.domain || domainForLocale(locale),\n\t\t...(normalizedMathSpeech?.style ? { style: normalizedMathSpeech.style } : {}),\n\t\tmodality: \"speech\",\n\t\tmarkup,\n\t});\n\tawait sre.engineReady?.();\n};\n\nconst runSerializedSreOperation = async <T>(\n\toperation: () => Promise<T>,\n): Promise<T> => {\n\tconst result = sreOperationQueue.then(operation, operation);\n\tsreOperationQueue = result.then(\n\t\t() => undefined,\n\t\t() => undefined,\n\t);\n\treturn result;\n};\n\nconst toSpeechWithSreSettings = async (\n\tsre: SpeechRuleEngineApi,\n\tmathml: string,\n\tlanguage: string | undefined,\n\tmarkup: \"none\" | \"ssml\",\n\tmathSpeech?: SREMathSpeechOptions,\n): Promise<string> =>\n\trunSerializedSreOperation(async () => {\n\t\tawait setupSre(sre, language, markup, mathSpeech);\n\t\treturn sre.toSpeech(mathml);\n\t});\n\n// SRE's SSML rendering wraps the spoken words in highlight-safe structural tags\n// (`speak`, `prosody`, `say-as interpret-as=\"character\"`, `break`), all handled\n// by `extractSpokenText`, so word-boundary offsets into the raw SSML still map\n// back to spoken tokens. SRE also prepends an XML prolog (`<?xml …?>`); AWS\n// Polly and Google Cloud TTS reject SSML that does not begin with `<speak>`, so\n// we strip the prolog here. The stripped string is the single source of truth:\n// it is both what the provider receives and what the alignment tokenizes, so\n// offsets stay consistent. We only accept output that contains a `<speak>`\n// element, so a bad/empty render degrades to plain text.\nconst stripXmlProlog = (ssml: string): string =>\n\tssml.replace(/^\\s*<\\?xml[^>]*\\?>\\s*/i, \"\");\n\nconst generateMathSsml = async (\n\tsre: SpeechRuleEngineApi,\n\tmathml: string,\n\tlanguage?: string,\n\tmathSpeech?: SREMathSpeechOptions,\n): Promise<string | null> => {\n\ttry {\n\t\tconst ssml = stripXmlProlog(\n\t\t\tawait toSpeechWithSreSettings(sre, mathml, language, \"ssml\", mathSpeech),\n\t\t);\n\t\treturn ssml.includes(\"<speak\") ? ssml : null;\n\t} catch (error) {\n\t\tconsole.debug(\"[TTSService] Math SSML generation failed; using plain\", {\n\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t});\n\t\treturn null;\n\t}\n};\n\nexport const resolveMathSpeechFromChunks = async (\n\tchunks: MathAwareSpeechChunk[],\n\toptions: ResolveMathSpeechOptions = {},\n): Promise<ResolvedMathSpeech> => {\n\tif (!chunks.some((chunk) => chunk.type === \"math\")) {\n\t\treturn {\n\t\t\tspeechText: normalizeTextForSpeech(\n\t\t\t\tchunks\n\t\t\t\t\t.map((chunk) =>\n\t\t\t\t\t\tchunk.type === \"text\" ? chunk.text : chunk.fallbackText,\n\t\t\t\t\t)\n\t\t\t\t\t.join(\" \"),\n\t\t\t),\n\t\t\tusedMathSpeech: false,\n\t\t\tusedFallback: false,\n\t\t};\n\t}\n\n\tlet sre: SpeechRuleEngineApi | null = null;\n\tlet usedMathSpeech = false;\n\tlet usedFallback = false;\n\tconst loadSre = options.loadSre || defaultLoadSre;\n\tconst speechParts: string[] = [];\n\n\tfor (const chunk of chunks) {\n\t\tif (chunk.type === \"text\") {\n\t\t\tspeechParts.push(chunk.text);\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tif (!sre) {\n\t\t\t\tsre = await loadSre();\n\t\t\t}\n\t\t\tconst speech = normalizeTextForSpeech(\n\t\t\t\tawait toSpeechWithSreSettings(\n\t\t\t\t\tsre,\n\t\t\t\t\tchunk.mathml,\n\t\t\t\t\toptions.language,\n\t\t\t\t\t\"none\",\n\t\t\t\t\toptions.mathSpeech,\n\t\t\t\t),\n\t\t\t);\n\t\t\tif (speech) {\n\t\t\t\tspeechParts.push(speech);\n\t\t\t\tusedMathSpeech = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tusedFallback = true;\n\t\t\tspeechParts.push(chunk.fallbackText);\n\t\t} catch (error) {\n\t\t\tusedFallback = true;\n\t\t\tconsole.debug(\n\t\t\t\t\"[TTSService] Math speech generation failed; using visible fallback\",\n\t\t\t\t{\n\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t},\n\t\t\t);\n\t\t\tspeechParts.push(chunk.fallbackText);\n\t\t}\n\t}\n\n\t// Generated-SSML path: only meaningful for a single math chunk (the runtime\n\t// composes one chunk per equation). Computed after the plain pass so the\n\t// markup: \"none\" `speechText` above is unchanged. The \"none\" → \"ssml\"\n\t// engine reconfiguration happens sequentially within this call.\n\tconst speechText = normalizeTextForSpeech(speechParts.join(\" \"));\n\tlet ssml: string | undefined;\n\tif (\n\t\toptions.produceSsml &&\n\t\tsre &&\n\t\tusedMathSpeech &&\n\t\tchunks.length === 1 &&\n\t\tchunks[0].type === \"math\"\n\t) {\n\t\tconst generated = await generateMathSsml(\n\t\t\tsre,\n\t\t\tchunks[0].mathml,\n\t\t\toptions.language,\n\t\t\toptions.mathSpeech,\n\t\t);\n\t\tssml = generated ?? undefined;\n\t}\n\n\treturn {\n\t\tspeechText,\n\t\tusedMathSpeech,\n\t\tusedFallback,\n\t\tssml,\n\t};\n};\n"]}
@@ -0,0 +1 @@
1
+ export declare const canonicalizeMathML: (input: string) => string | null;
@@ -0,0 +1,170 @@
1
+ const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
2
+ const ALLOWED_MATHML_ELEMENTS = new Set([
3
+ "math",
4
+ "maction",
5
+ "menclose",
6
+ "merror",
7
+ "mfenced",
8
+ "mfrac",
9
+ "mi",
10
+ "mmultiscripts",
11
+ "mn",
12
+ "mo",
13
+ "mover",
14
+ "mpadded",
15
+ "mphantom",
16
+ "mprescripts",
17
+ "mroot",
18
+ "mrow",
19
+ "ms",
20
+ "mspace",
21
+ "msqrt",
22
+ "mstyle",
23
+ "msub",
24
+ "msubsup",
25
+ "msup",
26
+ "mtable",
27
+ "mtd",
28
+ "mtext",
29
+ "mtr",
30
+ "munder",
31
+ "munderover",
32
+ "none",
33
+ "semantics",
34
+ "annotation",
35
+ "annotation-xml",
36
+ ]);
37
+ const ALLOWED_ATTRIBUTES = new Set([
38
+ "accent",
39
+ "accentunder",
40
+ "align",
41
+ "bevelled",
42
+ "class",
43
+ "close",
44
+ "columnalign",
45
+ "columnlines",
46
+ "columnspacing",
47
+ "columnspan",
48
+ "denomalign",
49
+ "depth",
50
+ "dir",
51
+ "display",
52
+ "encoding",
53
+ "fence",
54
+ "height",
55
+ "href",
56
+ "id",
57
+ "largeop",
58
+ "length",
59
+ "linethickness",
60
+ "lspace",
61
+ "mathbackground",
62
+ "mathcolor",
63
+ "mathsize",
64
+ "mathvariant",
65
+ "maxsize",
66
+ "minsize",
67
+ "movablelimits",
68
+ "notation",
69
+ "numalign",
70
+ "open",
71
+ "rowalign",
72
+ "rowlines",
73
+ "rowspacing",
74
+ "rowspan",
75
+ "rspace",
76
+ "scriptlevel",
77
+ "scriptminsize",
78
+ "scriptsizemultiplier",
79
+ "selection",
80
+ "separator",
81
+ "separators",
82
+ "stretchy",
83
+ "subscriptshift",
84
+ "supscriptshift",
85
+ "symmetric",
86
+ "width",
87
+ "xmlns",
88
+ "xml:lang",
89
+ ]);
90
+ const escapeText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
91
+ const escapeAttribute = (value) => escapeText(value).replace(/"/g, "&quot;");
92
+ const isUnsafeAttribute = (name, value) => {
93
+ const lowerName = name.toLowerCase();
94
+ const lowerValue = value.trim().toLowerCase();
95
+ if (lowerName.startsWith("on"))
96
+ return true;
97
+ if (!ALLOWED_ATTRIBUTES.has(lowerName))
98
+ return true;
99
+ if (lowerValue.includes("javascript:"))
100
+ return true;
101
+ if (lowerValue.includes("data:text/html"))
102
+ return true;
103
+ return false;
104
+ };
105
+ const isAllowedMathElement = (element) => {
106
+ const localName = element.localName.toLowerCase();
107
+ if (!ALLOWED_MATHML_ELEMENTS.has(localName))
108
+ return false;
109
+ const namespace = element.namespaceURI;
110
+ return (!namespace ||
111
+ namespace === MATHML_NAMESPACE ||
112
+ namespace === "http://www.w3.org/1999/xhtml");
113
+ };
114
+ const serializeMathNode = (node) => {
115
+ if (node.nodeType === Node.TEXT_NODE) {
116
+ return escapeText(node.textContent || "");
117
+ }
118
+ if (node.nodeType === Node.COMMENT_NODE) {
119
+ return "";
120
+ }
121
+ if (node.nodeType !== Node.ELEMENT_NODE) {
122
+ return null;
123
+ }
124
+ const element = node;
125
+ if (!isAllowedMathElement(element))
126
+ return null;
127
+ const tagName = element.localName.toLowerCase();
128
+ const attrs = [];
129
+ for (const attr of Array.from(element.attributes)) {
130
+ const name = attr.name.toLowerCase();
131
+ if (isUnsafeAttribute(name, attr.value))
132
+ continue;
133
+ if (name === "xmlns" && attr.value !== MATHML_NAMESPACE)
134
+ return null;
135
+ attrs.push(`${name}="${escapeAttribute(attr.value)}"`);
136
+ }
137
+ if (tagName === "math" && !attrs.some((attr) => attr.startsWith("xmlns="))) {
138
+ attrs.unshift(`xmlns="${MATHML_NAMESPACE}"`);
139
+ }
140
+ const children = [];
141
+ for (const child of Array.from(element.childNodes)) {
142
+ const serialized = serializeMathNode(child);
143
+ if (serialized === null)
144
+ return null;
145
+ children.push(serialized);
146
+ }
147
+ const attrText = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
148
+ return `<${tagName}${attrText}>${children.join("")}</${tagName}>`;
149
+ };
150
+ const parseMathML = (input) => {
151
+ if (typeof DOMParser !== "undefined") {
152
+ return new DOMParser().parseFromString(input, "application/xml");
153
+ }
154
+ return null;
155
+ };
156
+ export const canonicalizeMathML = (input) => {
157
+ const trimmed = String(input || "").trim();
158
+ if (!trimmed)
159
+ return null;
160
+ const parsed = parseMathML(trimmed);
161
+ if (!parsed)
162
+ return null;
163
+ if (parsed.getElementsByTagName("parsererror").length > 0)
164
+ return null;
165
+ const root = parsed.documentElement;
166
+ if (!root || root.localName.toLowerCase() !== "math")
167
+ return null;
168
+ return serializeMathNode(root);
169
+ };
170
+ //# sourceMappingURL=mathml-sanitization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mathml-sanitization.js","sourceRoot":"","sources":["../../../src/services/tts/mathml-sanitization.ts"],"names":[],"mappings":"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","sourcesContent":["const MATHML_NAMESPACE = \"http://www.w3.org/1998/Math/MathML\";\n\nconst ALLOWED_MATHML_ELEMENTS = new Set([\n\t\"math\",\n\t\"maction\",\n\t\"menclose\",\n\t\"merror\",\n\t\"mfenced\",\n\t\"mfrac\",\n\t\"mi\",\n\t\"mmultiscripts\",\n\t\"mn\",\n\t\"mo\",\n\t\"mover\",\n\t\"mpadded\",\n\t\"mphantom\",\n\t\"mprescripts\",\n\t\"mroot\",\n\t\"mrow\",\n\t\"ms\",\n\t\"mspace\",\n\t\"msqrt\",\n\t\"mstyle\",\n\t\"msub\",\n\t\"msubsup\",\n\t\"msup\",\n\t\"mtable\",\n\t\"mtd\",\n\t\"mtext\",\n\t\"mtr\",\n\t\"munder\",\n\t\"munderover\",\n\t\"none\",\n\t\"semantics\",\n\t\"annotation\",\n\t\"annotation-xml\",\n]);\n\nconst ALLOWED_ATTRIBUTES = new Set([\n\t\"accent\",\n\t\"accentunder\",\n\t\"align\",\n\t\"bevelled\",\n\t\"class\",\n\t\"close\",\n\t\"columnalign\",\n\t\"columnlines\",\n\t\"columnspacing\",\n\t\"columnspan\",\n\t\"denomalign\",\n\t\"depth\",\n\t\"dir\",\n\t\"display\",\n\t\"encoding\",\n\t\"fence\",\n\t\"height\",\n\t\"href\",\n\t\"id\",\n\t\"largeop\",\n\t\"length\",\n\t\"linethickness\",\n\t\"lspace\",\n\t\"mathbackground\",\n\t\"mathcolor\",\n\t\"mathsize\",\n\t\"mathvariant\",\n\t\"maxsize\",\n\t\"minsize\",\n\t\"movablelimits\",\n\t\"notation\",\n\t\"numalign\",\n\t\"open\",\n\t\"rowalign\",\n\t\"rowlines\",\n\t\"rowspacing\",\n\t\"rowspan\",\n\t\"rspace\",\n\t\"scriptlevel\",\n\t\"scriptminsize\",\n\t\"scriptsizemultiplier\",\n\t\"selection\",\n\t\"separator\",\n\t\"separators\",\n\t\"stretchy\",\n\t\"subscriptshift\",\n\t\"supscriptshift\",\n\t\"symmetric\",\n\t\"width\",\n\t\"xmlns\",\n\t\"xml:lang\",\n]);\n\nconst escapeText = (value: string): string =>\n\tvalue.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n\nconst escapeAttribute = (value: string): string =>\n\tescapeText(value).replace(/\"/g, \"&quot;\");\n\nconst isUnsafeAttribute = (name: string, value: string): boolean => {\n\tconst lowerName = name.toLowerCase();\n\tconst lowerValue = value.trim().toLowerCase();\n\tif (lowerName.startsWith(\"on\")) return true;\n\tif (!ALLOWED_ATTRIBUTES.has(lowerName)) return true;\n\tif (lowerValue.includes(\"javascript:\")) return true;\n\tif (lowerValue.includes(\"data:text/html\")) return true;\n\treturn false;\n};\n\nconst isAllowedMathElement = (element: Element): boolean => {\n\tconst localName = element.localName.toLowerCase();\n\tif (!ALLOWED_MATHML_ELEMENTS.has(localName)) return false;\n\tconst namespace = element.namespaceURI;\n\treturn (\n\t\t!namespace ||\n\t\tnamespace === MATHML_NAMESPACE ||\n\t\tnamespace === \"http://www.w3.org/1999/xhtml\"\n\t);\n};\n\nconst serializeMathNode = (node: Node): string | null => {\n\tif (node.nodeType === Node.TEXT_NODE) {\n\t\treturn escapeText(node.textContent || \"\");\n\t}\n\tif (node.nodeType === Node.COMMENT_NODE) {\n\t\treturn \"\";\n\t}\n\tif (node.nodeType !== Node.ELEMENT_NODE) {\n\t\treturn null;\n\t}\n\tconst element = node as Element;\n\tif (!isAllowedMathElement(element)) return null;\n\tconst tagName = element.localName.toLowerCase();\n\tconst attrs: string[] = [];\n\tfor (const attr of Array.from(element.attributes)) {\n\t\tconst name = attr.name.toLowerCase();\n\t\tif (isUnsafeAttribute(name, attr.value)) continue;\n\t\tif (name === \"xmlns\" && attr.value !== MATHML_NAMESPACE) return null;\n\t\tattrs.push(`${name}=\"${escapeAttribute(attr.value)}\"`);\n\t}\n\tif (tagName === \"math\" && !attrs.some((attr) => attr.startsWith(\"xmlns=\"))) {\n\t\tattrs.unshift(`xmlns=\"${MATHML_NAMESPACE}\"`);\n\t}\n\tconst children: string[] = [];\n\tfor (const child of Array.from(element.childNodes)) {\n\t\tconst serialized = serializeMathNode(child);\n\t\tif (serialized === null) return null;\n\t\tchildren.push(serialized);\n\t}\n\tconst attrText = attrs.length > 0 ? ` ${attrs.join(\" \")}` : \"\";\n\treturn `<${tagName}${attrText}>${children.join(\"\")}</${tagName}>`;\n};\n\nconst parseMathML = (input: string): Document | null => {\n\tif (typeof DOMParser !== \"undefined\") {\n\t\treturn new DOMParser().parseFromString(input, \"application/xml\");\n\t}\n\treturn null;\n};\n\nexport const canonicalizeMathML = (input: string): string | null => {\n\tconst trimmed = String(input || \"\").trim();\n\tif (!trimmed) return null;\n\tconst parsed = parseMathML(trimmed);\n\tif (!parsed) return null;\n\tif (parsed.getElementsByTagName(\"parsererror\").length > 0) return null;\n\tconst root = parsed.documentElement;\n\tif (!root || root.localName.toLowerCase() !== \"math\") return null;\n\treturn serializeMathNode(root);\n};\n"]}
@@ -0,0 +1,33 @@
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
+ */
27
+ export interface ExtractedSpokenText {
28
+ spokenText: string;
29
+ rawToSpokenOffsetMap: Map<number, number>;
30
+ unsupportedSemantic: boolean;
31
+ hasMarkup: boolean;
32
+ }
33
+ export declare const extractSpokenText: (speechText: string) => ExtractedSpokenText;
@@ -0,0 +1,159 @@
1
+ import { normalizeTextForSpeech } from "../text-processing.js";
2
+ const STRUCTURAL_SSML_TAGS = new Set([
3
+ "speak",
4
+ "p",
5
+ "s",
6
+ "emphasis",
7
+ "prosody",
8
+ "say-as",
9
+ "voice",
10
+ "lang",
11
+ ]);
12
+ const UNSUPPORTED_SEMANTIC_SSML_TAGS = new Set(["audio", "phoneme"]);
13
+ const decodeXmlEntities = (value) => value
14
+ .replace(/&quot;/g, '"')
15
+ .replace(/&apos;/g, "'")
16
+ .replace(/&lt;/g, "<")
17
+ .replace(/&gt;/g, ">")
18
+ .replace(/&nbsp;/g, "\u00a0")
19
+ .replace(/&#160;/g, "\u00a0")
20
+ .replace(/&amp;/g, "&");
21
+ const appendSourceText = (chars, value, rawOffset) => {
22
+ for (let i = 0; i < value.length; i++) {
23
+ chars.push({
24
+ char: value[i],
25
+ rawOffset: rawOffset === null ? null : rawOffset + i,
26
+ });
27
+ }
28
+ };
29
+ const appendDecodedXmlEntity = (chars, source, rawOffset) => {
30
+ const match = source
31
+ .slice(rawOffset)
32
+ .match(/^&(quot|apos|lt|gt|nbsp|amp|#160);/i);
33
+ if (!match)
34
+ return 0;
35
+ appendSourceText(chars, decodeXmlEntities(match[0]), rawOffset);
36
+ return match[0].length;
37
+ };
38
+ const appendSpace = (chars) => {
39
+ chars.push({ char: " ", rawOffset: null });
40
+ };
41
+ const normalizeSourceChars = (chars) => {
42
+ const rawToSpokenOffsetMap = new Map();
43
+ const out = [];
44
+ let lastWasWhitespace = true;
45
+ for (const sourceChar of chars) {
46
+ const isWhitespace = /\s/.test(sourceChar.char);
47
+ if (isWhitespace) {
48
+ if (!lastWasWhitespace && out.length > 0) {
49
+ out.push(" ");
50
+ if (sourceChar.rawOffset !== null) {
51
+ rawToSpokenOffsetMap.set(sourceChar.rawOffset, out.length - 1);
52
+ }
53
+ }
54
+ lastWasWhitespace = true;
55
+ continue;
56
+ }
57
+ out.push(sourceChar.char);
58
+ if (sourceChar.rawOffset !== null) {
59
+ rawToSpokenOffsetMap.set(sourceChar.rawOffset, out.length - 1);
60
+ }
61
+ lastWasWhitespace = false;
62
+ }
63
+ while (out.length > 0 && out[out.length - 1] === " ") {
64
+ out.pop();
65
+ }
66
+ // Drop any raw→spoken entries that now point past the trimmed end; otherwise
67
+ // a boundary at a trailing space would map to an out-of-range spoken offset.
68
+ for (const [rawOffset, spokenOffset] of rawToSpokenOffsetMap) {
69
+ if (spokenOffset >= out.length)
70
+ rawToSpokenOffsetMap.delete(rawOffset);
71
+ }
72
+ return { text: out.join(""), rawToSpokenOffsetMap };
73
+ };
74
+ const getTagName = (tagSource) => {
75
+ const match = tagSource.match(/^<\/?\s*([A-Za-z0-9:-]+)/);
76
+ return match?.[1]?.toLowerCase() || "";
77
+ };
78
+ const getAttribute = (tagSource, name) => {
79
+ const pattern = new RegExp(`${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, "i");
80
+ const match = tagSource.match(pattern);
81
+ return decodeXmlEntities(match?.[2] ?? match?.[3] ?? "") || null;
82
+ };
83
+ export const extractSpokenText = (speechText) => {
84
+ const sourceChars = [];
85
+ let unsupportedSemantic = false;
86
+ let hasMarkup = false;
87
+ let index = 0;
88
+ while (index < speechText.length) {
89
+ const char = speechText[index];
90
+ if (char !== "<") {
91
+ if (char === "&") {
92
+ const consumed = appendDecodedXmlEntity(sourceChars, speechText, index);
93
+ if (consumed > 0) {
94
+ index += consumed;
95
+ continue;
96
+ }
97
+ }
98
+ appendSourceText(sourceChars, char, index);
99
+ index++;
100
+ continue;
101
+ }
102
+ const closeIndex = speechText.indexOf(">", index + 1);
103
+ if (closeIndex < 0) {
104
+ appendSourceText(sourceChars, char, index);
105
+ index++;
106
+ continue;
107
+ }
108
+ hasMarkup = true;
109
+ const tagSource = speechText.slice(index, closeIndex + 1);
110
+ const tagName = getTagName(tagSource);
111
+ const isClosing = /^<\s*\//.test(tagSource);
112
+ const isSelfClosing = /\/\s*>$/.test(tagSource);
113
+ if (!isClosing && tagName === "sub") {
114
+ const alias = getAttribute(tagSource, "alias");
115
+ if (!alias) {
116
+ unsupportedSemantic = true;
117
+ index = closeIndex + 1;
118
+ continue;
119
+ }
120
+ appendSourceText(sourceChars, alias, null);
121
+ const closingSub = speechText
122
+ .toLowerCase()
123
+ .indexOf("</sub>", closeIndex + 1);
124
+ index = closingSub >= 0 ? closingSub + "</sub>".length : closeIndex + 1;
125
+ continue;
126
+ }
127
+ if (!isClosing && tagName === "break") {
128
+ appendSpace(sourceChars);
129
+ index = closeIndex + 1;
130
+ continue;
131
+ }
132
+ if (!isClosing && UNSUPPORTED_SEMANTIC_SSML_TAGS.has(tagName)) {
133
+ unsupportedSemantic = true;
134
+ if (isSelfClosing) {
135
+ index = closeIndex + 1;
136
+ continue;
137
+ }
138
+ }
139
+ if (tagName &&
140
+ !STRUCTURAL_SSML_TAGS.has(tagName) &&
141
+ !UNSUPPORTED_SEMANTIC_SSML_TAGS.has(tagName) &&
142
+ tagName !== "break" &&
143
+ tagName !== "sub") {
144
+ unsupportedSemantic = true;
145
+ }
146
+ if (!isClosing && (tagName === "p" || tagName === "s")) {
147
+ appendSpace(sourceChars);
148
+ }
149
+ index = closeIndex + 1;
150
+ }
151
+ const normalized = normalizeSourceChars(sourceChars);
152
+ return {
153
+ spokenText: normalizeTextForSpeech(normalized.text),
154
+ rawToSpokenOffsetMap: normalized.rawToSpokenOffsetMap,
155
+ unsupportedSemantic,
156
+ hasMarkup,
157
+ };
158
+ };
159
+ //# sourceMappingURL=spoken-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spoken-text.js","sourceRoot":"","sources":["../../../../src/services/tts/ssml/spoken-text.ts"],"names":[],"mappings":"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","sourcesContent":["import { normalizeTextForSpeech } from \"../text-processing.js\";\n\n/**\n * Shared SSML → spoken-text extractor.\n *\n * Strips SSML markup to the words a TTS engine would actually voice, while\n * recording a `rawToSpokenOffsetMap` from raw-SSML character offsets to\n * normalized spoken-text offsets. This is the single source of truth for SSML\n * stripping used by:\n * - the catalog span aligner (`catalog-span-alignment.ts`),\n * - the math speech tokenizer (`math-alignment/speech-tokenizer.ts`),\n * - the runtime generated-speech path (PIE-623).\n *\n * Keeping one implementation guarantees provider word-boundary offsets (which\n * are into the raw SSML string) map to the same spoken text on every path.\n *\n * Tag handling:\n * - Structural tags (`speak`, `p`, `s`, `emphasis`, `prosody`, `say-as`,\n * `voice`, `lang`) are transparent; their text content flows through.\n * `p`/`s` open tags inject a word boundary space.\n * - `break` injects a space (a pause is a word boundary).\n * - `sub alias=\"…\"` substitutes the alias text (and skips the original).\n * `sub` with no alias marks the content unsupported for word tracking.\n * - Semantic tags that change *what* is spoken without leaving trackable\n * text (`audio`, `phoneme`) mark the content unsupported.\n * - Any other unknown element marks the content unsupported (degrade to\n * coarse region highlighting rather than mis-track).\n */\nexport interface ExtractedSpokenText {\n\tspokenText: string;\n\trawToSpokenOffsetMap: Map<number, number>;\n\tunsupportedSemantic: boolean;\n\thasMarkup: boolean;\n}\n\ninterface SourceChar {\n\tchar: string;\n\trawOffset: number | null;\n}\n\nconst STRUCTURAL_SSML_TAGS = new Set([\n\t\"speak\",\n\t\"p\",\n\t\"s\",\n\t\"emphasis\",\n\t\"prosody\",\n\t\"say-as\",\n\t\"voice\",\n\t\"lang\",\n]);\n\nconst UNSUPPORTED_SEMANTIC_SSML_TAGS = new Set([\"audio\", \"phoneme\"]);\n\nconst decodeXmlEntities = (value: string): string =>\n\tvalue\n\t\t.replace(/&quot;/g, '\"')\n\t\t.replace(/&apos;/g, \"'\")\n\t\t.replace(/&lt;/g, \"<\")\n\t\t.replace(/&gt;/g, \">\")\n\t\t.replace(/&nbsp;/g, \"\\u00a0\")\n\t\t.replace(/&#160;/g, \"\\u00a0\")\n\t\t.replace(/&amp;/g, \"&\");\n\nconst appendSourceText = (\n\tchars: SourceChar[],\n\tvalue: string,\n\trawOffset: number | null,\n): void => {\n\tfor (let i = 0; i < value.length; i++) {\n\t\tchars.push({\n\t\t\tchar: value[i],\n\t\t\trawOffset: rawOffset === null ? null : rawOffset + i,\n\t\t});\n\t}\n};\n\nconst appendDecodedXmlEntity = (\n\tchars: SourceChar[],\n\tsource: string,\n\trawOffset: number,\n): number => {\n\tconst match = source\n\t\t.slice(rawOffset)\n\t\t.match(/^&(quot|apos|lt|gt|nbsp|amp|#160);/i);\n\tif (!match) return 0;\n\tappendSourceText(chars, decodeXmlEntities(match[0]), rawOffset);\n\treturn match[0].length;\n};\n\nconst appendSpace = (chars: SourceChar[]): void => {\n\tchars.push({ char: \" \", rawOffset: null });\n};\n\nconst normalizeSourceChars = (\n\tchars: SourceChar[],\n): { text: string; rawToSpokenOffsetMap: Map<number, number> } => {\n\tconst rawToSpokenOffsetMap = new Map<number, number>();\n\tconst out: string[] = [];\n\tlet lastWasWhitespace = true;\n\n\tfor (const sourceChar of chars) {\n\t\tconst isWhitespace = /\\s/.test(sourceChar.char);\n\t\tif (isWhitespace) {\n\t\t\tif (!lastWasWhitespace && out.length > 0) {\n\t\t\t\tout.push(\" \");\n\t\t\t\tif (sourceChar.rawOffset !== null) {\n\t\t\t\t\trawToSpokenOffsetMap.set(sourceChar.rawOffset, out.length - 1);\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastWasWhitespace = true;\n\t\t\tcontinue;\n\t\t}\n\t\tout.push(sourceChar.char);\n\t\tif (sourceChar.rawOffset !== null) {\n\t\t\trawToSpokenOffsetMap.set(sourceChar.rawOffset, out.length - 1);\n\t\t}\n\t\tlastWasWhitespace = false;\n\t}\n\n\twhile (out.length > 0 && out[out.length - 1] === \" \") {\n\t\tout.pop();\n\t}\n\n\t// Drop any raw→spoken entries that now point past the trimmed end; otherwise\n\t// a boundary at a trailing space would map to an out-of-range spoken offset.\n\tfor (const [rawOffset, spokenOffset] of rawToSpokenOffsetMap) {\n\t\tif (spokenOffset >= out.length) rawToSpokenOffsetMap.delete(rawOffset);\n\t}\n\n\treturn { text: out.join(\"\"), rawToSpokenOffsetMap };\n};\n\nconst getTagName = (tagSource: string): string => {\n\tconst match = tagSource.match(/^<\\/?\\s*([A-Za-z0-9:-]+)/);\n\treturn match?.[1]?.toLowerCase() || \"\";\n};\n\nconst getAttribute = (tagSource: string, name: string): string | null => {\n\tconst pattern = new RegExp(`${name}\\\\s*=\\\\s*(\"([^\"]*)\"|'([^']*)')`, \"i\");\n\tconst match = tagSource.match(pattern);\n\treturn decodeXmlEntities(match?.[2] ?? match?.[3] ?? \"\") || null;\n};\n\nexport const extractSpokenText = (speechText: string): ExtractedSpokenText => {\n\tconst sourceChars: SourceChar[] = [];\n\tlet unsupportedSemantic = false;\n\tlet hasMarkup = false;\n\tlet index = 0;\n\n\twhile (index < speechText.length) {\n\t\tconst char = speechText[index];\n\t\tif (char !== \"<\") {\n\t\t\tif (char === \"&\") {\n\t\t\t\tconst consumed = appendDecodedXmlEntity(sourceChars, speechText, index);\n\t\t\t\tif (consumed > 0) {\n\t\t\t\t\tindex += consumed;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\tappendSourceText(sourceChars, char, index);\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst closeIndex = speechText.indexOf(\">\", index + 1);\n\t\tif (closeIndex < 0) {\n\t\t\tappendSourceText(sourceChars, char, index);\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\n\t\thasMarkup = true;\n\t\tconst tagSource = speechText.slice(index, closeIndex + 1);\n\t\tconst tagName = getTagName(tagSource);\n\t\tconst isClosing = /^<\\s*\\//.test(tagSource);\n\t\tconst isSelfClosing = /\\/\\s*>$/.test(tagSource);\n\n\t\tif (!isClosing && tagName === \"sub\") {\n\t\t\tconst alias = getAttribute(tagSource, \"alias\");\n\t\t\tif (!alias) {\n\t\t\t\tunsupportedSemantic = true;\n\t\t\t\tindex = closeIndex + 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tappendSourceText(sourceChars, alias, null);\n\t\t\tconst closingSub = speechText\n\t\t\t\t.toLowerCase()\n\t\t\t\t.indexOf(\"</sub>\", closeIndex + 1);\n\t\t\tindex = closingSub >= 0 ? closingSub + \"</sub>\".length : closeIndex + 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!isClosing && tagName === \"break\") {\n\t\t\tappendSpace(sourceChars);\n\t\t\tindex = closeIndex + 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!isClosing && UNSUPPORTED_SEMANTIC_SSML_TAGS.has(tagName)) {\n\t\t\tunsupportedSemantic = true;\n\t\t\tif (isSelfClosing) {\n\t\t\t\tindex = closeIndex + 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\tif (\n\t\t\ttagName &&\n\t\t\t!STRUCTURAL_SSML_TAGS.has(tagName) &&\n\t\t\t!UNSUPPORTED_SEMANTIC_SSML_TAGS.has(tagName) &&\n\t\t\ttagName !== \"break\" &&\n\t\t\ttagName !== \"sub\"\n\t\t) {\n\t\t\tunsupportedSemantic = true;\n\t\t}\n\n\t\tif (!isClosing && (tagName === \"p\" || tagName === \"s\")) {\n\t\t\tappendSpace(sourceChars);\n\t\t}\n\t\tindex = closeIndex + 1;\n\t}\n\n\tconst normalized = normalizeSourceChars(sourceChars);\n\treturn {\n\t\tspokenText: normalizeTextForSpeech(normalized.text),\n\t\trawToSpokenOffsetMap: normalized.rawToSpokenOffsetMap,\n\t\tunsupportedSemantic,\n\t\thasMarkup,\n\t};\n};\n"]}
@@ -8,7 +8,10 @@ export interface TextProcessingOptions {
8
8
  boundarySpacingMode?: BoundarySpacingMode;
9
9
  }
10
10
  export declare const normalizeTextForSpeech: (text: string) => string;
11
+ export declare const createSpeechAlignmentTokenPattern: () => RegExp;
11
12
  export declare const isElementHiddenForTTS: (element: Element) => boolean;
13
+ export declare const isNodeHiddenForTTS: (node: Node, root?: Element | null) => boolean;
14
+ export declare const shouldInsertWordBoundarySpace: (previousChar: string | null, nextChar: string | null, options?: TextProcessingOptions) => boolean;
12
15
  export declare const collectVisibleTextAndMap: (element: Element, options?: TextProcessingOptions) => {
13
16
  text: string;
14
17
  map: NormalizedTextMap;