@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,106 @@
1
+ import { resolveSpokenBoundaryOffset } from "../catalog-span-alignment.js";
2
+ import { resolveBoundaryToSpeechToken, tokenizeSpeechSource, } from "../math-alignment/speech-tokenizer.js";
3
+ const normalizeBoundaryWord = (word) => {
4
+ if (!word || /^<[^>]+>$/.test(word.trim()))
5
+ return null;
6
+ return (tokenizeSpeechSource({ speechText: word }).tokens[0]?.normalized || null);
7
+ };
8
+ // Resolve a provider boundary for a chunk that carries no catalog span
9
+ // alignment — notably the generated-SSML math path, whose `speechText` is a
10
+ // raw `<speak>` document. The provider's offset is an index into `speechText`
11
+ // exactly as it was sent (which may include SSML tags), so we tokenize that
12
+ // same string and let the shared speech tokenizer translate the offset into the
13
+ // extracted spoken text. Returns the boundary in spoken-text space alongside
14
+ // that spoken text, so the downstream word/offset checks share one coordinate
15
+ // system. The previous "direct" path treated the raw-SSML offset as a
16
+ // spoken-text offset and rejected every math boundary (PIE-623 regression).
17
+ const speechSourceBoundary = (chunk, event) => {
18
+ const tokenization = tokenizeSpeechSource({ speechText: chunk.speechText });
19
+ if (!Number.isFinite(event.position)) {
20
+ return { boundary: null, spokenText: tokenization.spokenText };
21
+ }
22
+ const resolved = resolveBoundaryToSpeechToken({
23
+ tokenization,
24
+ position: event.position,
25
+ length: event.length,
26
+ boundaryWord: event.word,
27
+ });
28
+ return {
29
+ boundary: resolved
30
+ ? { start: resolved.start, length: resolved.length }
31
+ : null,
32
+ spokenText: tokenization.spokenText,
33
+ };
34
+ };
35
+ const tokenAtOffsetMatches = (speechText, start, normalizedWord) => {
36
+ if (!normalizedWord)
37
+ return true;
38
+ const token = tokenizeSpeechSource({ speechText }).tokens.find((candidate) => candidate.start <= start && start < candidate.end);
39
+ return token?.normalized === normalizedWord;
40
+ };
41
+ export const normalizeBoundaryEvent = (chunk, event) => {
42
+ const normalizedWord = normalizeBoundaryWord(event.word);
43
+ if (chunk.id !== event.chunkId) {
44
+ return {
45
+ chunkId: event.chunkId,
46
+ normalizedWord,
47
+ chunkSpokenStart: null,
48
+ chunkSpokenEnd: null,
49
+ confidence: 0,
50
+ reason: "boundary chunk id did not match highlight chunk",
51
+ };
52
+ }
53
+ if (event.providerOffsetSpace === "unknown" ||
54
+ chunk.offsetSpace === "unsupported") {
55
+ return {
56
+ chunkId: chunk.id,
57
+ normalizedWord,
58
+ chunkSpokenStart: null,
59
+ chunkSpokenEnd: null,
60
+ confidence: 0,
61
+ reason: "boundary offset space is unsupported",
62
+ };
63
+ }
64
+ let boundary;
65
+ let spokenText;
66
+ if (chunk.catalogAlignment) {
67
+ boundary = resolveSpokenBoundaryOffset(chunk.catalogAlignment, event.position, event.length, event.word);
68
+ spokenText = chunk.catalogAlignment.spokenText;
69
+ }
70
+ else {
71
+ const resolved = speechSourceBoundary(chunk, event);
72
+ boundary = resolved.boundary;
73
+ spokenText = resolved.spokenText;
74
+ }
75
+ if (!boundary) {
76
+ return {
77
+ chunkId: chunk.id,
78
+ normalizedWord,
79
+ chunkSpokenStart: null,
80
+ chunkSpokenEnd: null,
81
+ confidence: 0,
82
+ reason: "could not resolve provider boundary to chunk speech",
83
+ };
84
+ }
85
+ if (!tokenAtOffsetMatches(spokenText, boundary.start, normalizedWord)) {
86
+ return {
87
+ chunkId: chunk.id,
88
+ normalizedWord,
89
+ chunkSpokenStart: null,
90
+ chunkSpokenEnd: null,
91
+ confidence: 0,
92
+ reason: "could not resolve provider boundary to matching word",
93
+ };
94
+ }
95
+ return {
96
+ chunkId: chunk.id,
97
+ normalizedWord,
98
+ chunkSpokenStart: boundary.start,
99
+ chunkSpokenEnd: boundary.start + boundary.length,
100
+ confidence: normalizedWord ? 1 : 0.75,
101
+ reason: chunk.catalogAlignment
102
+ ? "resolved through catalog alignment"
103
+ : "resolved through speech tokenization",
104
+ };
105
+ };
106
+ //# sourceMappingURL=boundary-normalizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundary-normalizer.js","sourceRoot":"","sources":["../../../../src/services/tts/highlight-pipeline/boundary-normalizer.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolveSpokenBoundaryOffset } from \"../catalog-span-alignment.js\";\nimport {\n\tresolveBoundaryToSpeechToken,\n\ttokenizeSpeechSource,\n} from \"../math-alignment/speech-tokenizer.js\";\nimport type {\n\tNormalizedBoundaryEvent,\n\tTTSBoundaryEvent,\n\tTTSHighlightChunk,\n} from \"./types.js\";\n\nconst normalizeBoundaryWord = (word: string): string | null => {\n\tif (!word || /^<[^>]+>$/.test(word.trim())) return null;\n\treturn (\n\t\ttokenizeSpeechSource({ speechText: word }).tokens[0]?.normalized || null\n\t);\n};\n\n// Resolve a provider boundary for a chunk that carries no catalog span\n// alignment — notably the generated-SSML math path, whose `speechText` is a\n// raw `<speak>` document. The provider's offset is an index into `speechText`\n// exactly as it was sent (which may include SSML tags), so we tokenize that\n// same string and let the shared speech tokenizer translate the offset into the\n// extracted spoken text. Returns the boundary in spoken-text space alongside\n// that spoken text, so the downstream word/offset checks share one coordinate\n// system. The previous \"direct\" path treated the raw-SSML offset as a\n// spoken-text offset and rejected every math boundary (PIE-623 regression).\nconst speechSourceBoundary = (\n\tchunk: TTSHighlightChunk,\n\tevent: TTSBoundaryEvent,\n): { boundary: { start: number; length: number } | null; spokenText: string } => {\n\tconst tokenization = tokenizeSpeechSource({ speechText: chunk.speechText });\n\tif (!Number.isFinite(event.position)) {\n\t\treturn { boundary: null, spokenText: tokenization.spokenText };\n\t}\n\tconst resolved = resolveBoundaryToSpeechToken({\n\t\ttokenization,\n\t\tposition: event.position,\n\t\tlength: event.length,\n\t\tboundaryWord: event.word,\n\t});\n\treturn {\n\t\tboundary: resolved\n\t\t\t? { start: resolved.start, length: resolved.length }\n\t\t\t: null,\n\t\tspokenText: tokenization.spokenText,\n\t};\n};\n\nconst tokenAtOffsetMatches = (\n\tspeechText: string,\n\tstart: number,\n\tnormalizedWord: string | null,\n): boolean => {\n\tif (!normalizedWord) return true;\n\tconst token = tokenizeSpeechSource({ speechText }).tokens.find(\n\t\t(candidate) => candidate.start <= start && start < candidate.end,\n\t);\n\treturn token?.normalized === normalizedWord;\n};\n\nexport const normalizeBoundaryEvent = (\n\tchunk: TTSHighlightChunk,\n\tevent: TTSBoundaryEvent,\n): NormalizedBoundaryEvent => {\n\tconst normalizedWord = normalizeBoundaryWord(event.word);\n\tif (chunk.id !== event.chunkId) {\n\t\treturn {\n\t\t\tchunkId: event.chunkId,\n\t\t\tnormalizedWord,\n\t\t\tchunkSpokenStart: null,\n\t\t\tchunkSpokenEnd: null,\n\t\t\tconfidence: 0,\n\t\t\treason: \"boundary chunk id did not match highlight chunk\",\n\t\t};\n\t}\n\tif (\n\t\tevent.providerOffsetSpace === \"unknown\" ||\n\t\tchunk.offsetSpace === \"unsupported\"\n\t) {\n\t\treturn {\n\t\t\tchunkId: chunk.id,\n\t\t\tnormalizedWord,\n\t\t\tchunkSpokenStart: null,\n\t\t\tchunkSpokenEnd: null,\n\t\t\tconfidence: 0,\n\t\t\treason: \"boundary offset space is unsupported\",\n\t\t};\n\t}\n\tlet boundary: { start: number; length: number } | null;\n\tlet spokenText: string;\n\tif (chunk.catalogAlignment) {\n\t\tboundary = resolveSpokenBoundaryOffset(\n\t\t\tchunk.catalogAlignment,\n\t\t\tevent.position,\n\t\t\tevent.length,\n\t\t\tevent.word,\n\t\t);\n\t\tspokenText = chunk.catalogAlignment.spokenText;\n\t} else {\n\t\tconst resolved = speechSourceBoundary(chunk, event);\n\t\tboundary = resolved.boundary;\n\t\tspokenText = resolved.spokenText;\n\t}\n\tif (!boundary) {\n\t\treturn {\n\t\t\tchunkId: chunk.id,\n\t\t\tnormalizedWord,\n\t\t\tchunkSpokenStart: null,\n\t\t\tchunkSpokenEnd: null,\n\t\t\tconfidence: 0,\n\t\t\treason: \"could not resolve provider boundary to chunk speech\",\n\t\t};\n\t}\n\tif (!tokenAtOffsetMatches(spokenText, boundary.start, normalizedWord)) {\n\t\treturn {\n\t\t\tchunkId: chunk.id,\n\t\t\tnormalizedWord,\n\t\t\tchunkSpokenStart: null,\n\t\t\tchunkSpokenEnd: null,\n\t\t\tconfidence: 0,\n\t\t\treason: \"could not resolve provider boundary to matching word\",\n\t\t};\n\t}\n\treturn {\n\t\tchunkId: chunk.id,\n\t\tnormalizedWord,\n\t\tchunkSpokenStart: boundary.start,\n\t\tchunkSpokenEnd: boundary.start + boundary.length,\n\t\tconfidence: normalizedWord ? 1 : 0.75,\n\t\treason: chunk.catalogAlignment\n\t\t\t? \"resolved through catalog alignment\"\n\t\t\t: \"resolved through speech tokenization\",\n\t};\n};\n"]}
@@ -0,0 +1,6 @@
1
+ import type { TTSHighlightChunk, TTSHighlightPlan } from "./types.js";
2
+ export interface CreateTTSHighlightPlanArgs {
3
+ chunks: TTSHighlightChunk[];
4
+ mathTokenHighlighting?: boolean;
5
+ }
6
+ export declare const createTTSHighlightPlan: (args: CreateTTSHighlightPlanArgs) => TTSHighlightPlan;
@@ -0,0 +1,217 @@
1
+ import { normalizeBoundaryEvent } from "./boundary-normalizer.js";
2
+ import { createHighlightDecision } from "./highlight-policy.js";
3
+ import { resolveMathBoundaryTarget } from "./math-semantic-resolver.js";
4
+ import { resolveProseBoundaryTarget } from "./prose-boundary-resolver.js";
5
+ import { classifyMathHighlightCapability, createRenderedMathTargetResolver, } from "./rendered-math-target-resolver.js";
6
+ const regionTargetFor = (chunk) => chunk.regionRange
7
+ ? {
8
+ type: "range",
9
+ quality: "region",
10
+ range: chunk.regionRange,
11
+ }
12
+ : chunk.regionElement
13
+ ? {
14
+ type: "element",
15
+ quality: "region",
16
+ element: chunk.regionElement,
17
+ }
18
+ : null;
19
+ const expressionTargetFor = (planned, candidates = planned.chunk
20
+ .mathAlignments) => {
21
+ if (candidates.length !== 1)
22
+ return null;
23
+ const fallback = candidates[0]?.alignment.result.fallbackTarget;
24
+ return fallback ? planned.renderedMathTargetResolver(fallback) : null;
25
+ };
26
+ const mathCandidatesForTarget = (target, chunk) => {
27
+ if (!target)
28
+ return chunk.mathAlignments;
29
+ return chunk.mathAlignments.filter(({ element }) => {
30
+ try {
31
+ if (target.type === "range")
32
+ return target.range.intersectsNode(element);
33
+ if (target.type === "element") {
34
+ return target.element === element || target.element.contains(element);
35
+ }
36
+ return target.node.parentElement?.contains(element) || false;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ });
42
+ };
43
+ const missingChunkDecision = () => ({
44
+ activeTarget: null,
45
+ regionTarget: null,
46
+ quality: "region",
47
+ confidence: 0,
48
+ reason: "boundary chunk id was not found",
49
+ });
50
+ const soleEquationElement = (candidates) => (candidates.length === 1 ? candidates[0].element : null);
51
+ const everyCandidateIsTokenMode = (planned, candidates) => candidates.length > 0 &&
52
+ candidates.every(({ element }) => planned.mathCapabilityByElement.get(element) === "token");
53
+ const TEXT_NODE = 3;
54
+ // Safety policy for token-mode equations. A prose "range" target comes from the
55
+ // catalog span aligner, whose spoken→visible mapping inside an equation is
56
+ // heuristic and not trustworthy at glyph granularity. A multi-node range fully
57
+ // inside a token-mode equation is therefore rejected here in favor of holding
58
+ // the last token — painting it would be a likely false positive, and "false
59
+ // positives are worse than coarse highlighting." A single-text-node prose range
60
+ // is kept: it underlines one token cleanly and usefully fills in any token the
61
+ // math resolver missed. (The highlight coordinator no longer escalates word
62
+ // ranges to the whole <math> / <mjx-container>, so this is now a precision
63
+ // policy rather than flash-suppression — see HighlightCoordinator
64
+ // WORD_FALLBACK_SELECTOR.)
65
+ const wouldEscalateInsideTokenMath = (planned, target, candidates) => {
66
+ if (!target || target.type !== "range")
67
+ return false;
68
+ const { startContainer, endContainer } = target.range;
69
+ const singleTextNode = startContainer === endContainer && startContainer.nodeType === TEXT_NODE;
70
+ if (singleTextNode)
71
+ return false;
72
+ return candidates.some(({ element }) => planned.mathCapabilityByElement.get(element) === "token" &&
73
+ element.contains(startContainer) &&
74
+ element.contains(endContainer));
75
+ };
76
+ const createPlannedChunk = (chunk, mathTokenHighlighting) => ({
77
+ chunk,
78
+ regionTarget: regionTargetFor(chunk),
79
+ renderedMathTargetResolver: createRenderedMathTargetResolver(),
80
+ mathCapabilityByElement: new Map(chunk.mathAlignments.map(({ element }) => [
81
+ element,
82
+ // Forcing "expression" when per-token math highlighting is disabled
83
+ // routes every equation through the sticky whole-block path, so the
84
+ // formula is highlighted as one unit instead of glyph by glyph.
85
+ mathTokenHighlighting
86
+ ? classifyMathHighlightCapability(element)
87
+ : "expression",
88
+ ])),
89
+ heldTokenByElement: new Map(),
90
+ });
91
+ export const createTTSHighlightPlan = (args) => {
92
+ const mathTokenHighlighting = args.mathTokenHighlighting !== false;
93
+ const plannedChunksById = new Map(args.chunks.map((chunk) => [
94
+ chunk.id,
95
+ createPlannedChunk(chunk, mathTokenHighlighting),
96
+ ]));
97
+ return {
98
+ chunks: args.chunks,
99
+ resolveInitial(chunkId) {
100
+ const planned = plannedChunksById.get(chunkId);
101
+ if (!planned)
102
+ return missingChunkDecision();
103
+ const alignments = planned.chunk.mathAlignments;
104
+ const soleElement = alignments.length === 1 ? alignments[0].element : null;
105
+ const capability = soleElement
106
+ ? planned.mathCapabilityByElement.get(soleElement)
107
+ : null;
108
+ // Sticky mode. Only an equation we already know cannot be tracked per
109
+ // token gets the whole-expression paint up front. A token-mode equation
110
+ // stays on the region layer until its first token resolves, so the full
111
+ // equation never flashes before (or between) tokens.
112
+ if (capability === "expression") {
113
+ return createHighlightDecision({
114
+ semanticTarget: null,
115
+ expressionTarget: expressionTargetFor(planned),
116
+ regionTarget: planned.regionTarget,
117
+ confidence: 0.5,
118
+ reason: "initial expression (sticky expression mode)",
119
+ });
120
+ }
121
+ return createHighlightDecision({
122
+ semanticTarget: null,
123
+ expressionTarget: null,
124
+ regionTarget: planned.regionTarget,
125
+ confidence: 0,
126
+ reason: capability === "token"
127
+ ? "initial region (sticky token mode, awaiting first token)"
128
+ : "initial region fallback",
129
+ });
130
+ },
131
+ resolveBoundary(event) {
132
+ const planned = plannedChunksById.get(event.chunkId);
133
+ if (!planned)
134
+ return missingChunkDecision();
135
+ const { chunk } = planned;
136
+ const normalized = normalizeBoundaryEvent(chunk, event);
137
+ const proseTarget = resolveProseBoundaryTarget(chunk, normalized);
138
+ const mathCandidates = mathCandidatesForTarget(proseTarget, chunk);
139
+ const mathTarget = (!proseTarget || mathCandidates.length > 0
140
+ ? resolveMathBoundaryTarget(chunk, normalized, event, mathCandidates, planned.renderedMathTargetResolver)
141
+ : null) || null;
142
+ const mathTokenTarget = mathTokenHighlighting &&
143
+ mathTarget &&
144
+ mathTarget.quality !== "expression"
145
+ ? mathTarget
146
+ : null;
147
+ const mathExpressionFallback = mathTarget && mathTarget.quality === "expression" ? mathTarget : null;
148
+ // (1) A precise token always wins, and becomes the held target for its
149
+ // equation so a later gap can keep it lit instead of flashing the whole
150
+ // expression.
151
+ if (mathTokenTarget) {
152
+ const equationElement = soleEquationElement(mathCandidates);
153
+ if (equationElement) {
154
+ planned.heldTokenByElement.set(equationElement, mathTokenTarget);
155
+ }
156
+ return createHighlightDecision({
157
+ semanticTarget: mathTokenTarget,
158
+ expressionTarget: null,
159
+ regionTarget: planned.regionTarget,
160
+ confidence: normalized.confidence,
161
+ reason: normalized.reason,
162
+ });
163
+ }
164
+ // (2) A spoken word that maps to visible prose. The prose anchor is
165
+ // precise; a coarse math expression fallback must never override it.
166
+ // Exception: a multi-node prose range inside a token-mode equation
167
+ // would escalate to the whole expression, so it is rejected here and
168
+ // falls through to the token hold below. When per-token math
169
+ // highlighting is disabled, any prose range that lands inside a
170
+ // formula is likewise rejected so the formula stays a single block.
171
+ if (proseTarget &&
172
+ (mathTokenHighlighting
173
+ ? !wouldEscalateInsideTokenMath(planned, proseTarget, mathCandidates)
174
+ : mathCandidates.length === 0)) {
175
+ return createHighlightDecision({
176
+ semanticTarget: proseTarget,
177
+ expressionTarget: null,
178
+ regionTarget: planned.regionTarget,
179
+ confidence: normalized.confidence,
180
+ reason: normalized.reason,
181
+ });
182
+ }
183
+ // (3) A boundary inside a token-mode equation that did not map to a
184
+ // token — an SSML break, a pause, or a gap between spoken tokens. Hold
185
+ // the last token instead of flashing the whole expression; before the
186
+ // first token there is nothing to hold, so fall to the region layer.
187
+ // The whole expression is deliberately never painted in token mode.
188
+ if (everyCandidateIsTokenMode(planned, mathCandidates)) {
189
+ const equationElement = soleEquationElement(mathCandidates);
190
+ const heldToken = equationElement
191
+ ? (planned.heldTokenByElement.get(equationElement) ?? null)
192
+ : null;
193
+ return createHighlightDecision({
194
+ semanticTarget: heldToken,
195
+ expressionTarget: null,
196
+ regionTarget: planned.regionTarget,
197
+ confidence: normalized.confidence,
198
+ reason: heldToken
199
+ ? "hold last token (sticky token mode)"
200
+ : "region (sticky token mode, awaiting first token)",
201
+ });
202
+ }
203
+ // (4) An expression-mode (or mixed/ambiguous) math region: paint the
204
+ // whole equation. Stable for the equation's full duration because
205
+ // tokens never resolve here, so there is no flicker.
206
+ return createHighlightDecision({
207
+ semanticTarget: null,
208
+ expressionTarget: mathExpressionFallback ??
209
+ expressionTargetFor(planned, mathCandidates),
210
+ regionTarget: planned.regionTarget,
211
+ confidence: normalized.confidence,
212
+ reason: normalized.reason,
213
+ });
214
+ },
215
+ };
216
+ };
217
+ //# sourceMappingURL=create-highlight-plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-highlight-plan.js","sourceRoot":"","sources":["../../../../src/services/tts/highlight-pipeline/create-highlight-plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EACN,+BAA+B,EAC/B,gCAAgC,GAGhC,MAAM,oCAAoC,CAAC;AAgC5C,MAAM,eAAe,GAAG,CACvB,KAAwB,EACW,EAAE,CACrC,KAAK,CAAC,WAAW;IAChB,CAAC,CAAC;QACA,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,KAAK,CAAC,WAAW;KACxB;IACF,CAAC,CAAC,KAAK,CAAC,aAAa;QACpB,CAAC,CAAC;YACA,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,QAAQ;YACjB,OAAO,EAAE,KAAK,CAAC,aAAa;SAC5B;QACF,CAAC,CAAC,IAAI,CAAC;AAEV,MAAM,mBAAmB,GAAG,CAC3B,OAAqB,EACrB,aAAkD,OAAO,CAAC,KAAK;KAC7D,cAAc,EACmB,EAAE;IACrC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,cAAc,CAAC;IAChE,OAAO,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,0BAA0B,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvE,CAAC,CAAC;AAEF,MAAM,uBAAuB,GAAG,CAC/B,MAAwC,EACxC,KAAwB,EACc,EAAE;IACxC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC,cAAc,CAAC;IACzC,OAAO,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE;QAClD,IAAI,CAAC;YACJ,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;gBAAE,OAAO,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;YACzE,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC/B,OAAO,MAAM,CAAC,OAAO,KAAK,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YACvE,CAAC;YACD,OAAO,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,KAAK,CAAC;QACd,CAAC;IACF,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,oBAAoB,GAAG,GAAsB,EAAE,CAAC,CAAC;IACtD,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,IAAI;IAClB,OAAO,EAAE,QAAQ;IACjB,UAAU,EAAE,CAAC;IACb,MAAM,EAAE,iCAAiC;CACzC,CAAC,CAAC;AAEH,MAAM,mBAAmB,GAAG,CAC3B,UAA+C,EAC9B,EAAE,CAAC,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAE9E,MAAM,yBAAyB,GAAG,CACjC,OAAqB,EACrB,UAA+C,EACrC,EAAE,CACZ,UAAU,CAAC,MAAM,GAAG,CAAC;IACrB,UAAU,CAAC,KAAK,CACf,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,uBAAuB,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,OAAO,CACzE,CAAC;AAEH,MAAM,SAAS,GAAG,CAAC,CAAC;AAEpB,gFAAgF;AAChF,2EAA2E;AAC3E,+EAA+E;AAC/E,8EAA8E;AAC9E,4EAA4E;AAC5E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,kEAAkE;AAClE,2BAA2B;AAC3B,MAAM,4BAA4B,GAAG,CACpC,OAAqB,EACrB,MAAwC,EACxC,UAA+C,EACrC,EAAE;IACZ,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IACrD,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC;IACtD,MAAM,cAAc,GACnB,cAAc,KAAK,YAAY,IAAI,cAAc,CAAC,QAAQ,KAAK,SAAS,CAAC;IAC1E,IAAI,cAAc;QAAE,OAAO,KAAK,CAAC;IACjC,OAAO,UAAU,CAAC,IAAI,CACrB,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CACf,OAAO,CAAC,uBAAuB,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,OAAO;QACxD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;QAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAC/B,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,kBAAkB,GAAG,CAC1B,KAAwB,EACxB,qBAA8B,EACf,EAAE,CAAC,CAAC;IACnB,KAAK;IACL,YAAY,EAAE,eAAe,CAAC,KAAK,CAAC;IACpC,0BAA0B,EAAE,gCAAgC,EAAE;IAC9D,uBAAuB,EAAE,IAAI,GAAG,CAC/B,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QACzC,OAAO;QACP,oEAAoE;QACpE,oEAAoE;QACpE,gEAAgE;QAChE,qBAAqB;YACpB,CAAC,CAAC,+BAA+B,CAAC,OAAO,CAAC;YAC1C,CAAC,CAAC,YAAY;KACf,CAAC,CACF;IACD,kBAAkB,EAAE,IAAI,GAAG,EAAE;CAC7B,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACrC,IAAgC,EACb,EAAE;IACrB,MAAM,qBAAqB,GAAG,IAAI,CAAC,qBAAqB,KAAK,KAAK,CAAC;IACnE,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAChC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC1B,KAAK,CAAC,EAAE;QACR,kBAAkB,CAAC,KAAK,EAAE,qBAAqB,CAAC;KAChD,CAAC,CACF,CAAC;IACF,OAAO;QACN,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,cAAc,CAAC,OAAe;YAC7B,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC/C,IAAI,CAAC,OAAO;gBAAE,OAAO,oBAAoB,EAAE,CAAC;YAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC;YAChD,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;YAC3E,MAAM,UAAU,GAAG,WAAW;gBAC7B,CAAC,CAAC,OAAO,CAAC,uBAAuB,CAAC,GAAG,CAAC,WAAW,CAAC;gBAClD,CAAC,CAAC,IAAI,CAAC;YACR,sEAAsE;YACtE,wEAAwE;YACxE,wEAAwE;YACxE,qDAAqD;YACrD,IAAI,UAAU,KAAK,YAAY,EAAE,CAAC;gBACjC,OAAO,uBAAuB,CAAC;oBAC9B,cAAc,EAAE,IAAI;oBACpB,gBAAgB,EAAE,mBAAmB,CAAC,OAAO,CAAC;oBAC9C,YAAY,EAAE,OAAO,CAAC,YAAY;oBAClC,UAAU,EAAE,GAAG;oBACf,MAAM,EAAE,6CAA6C;iBACrD,CAAC,CAAC;YACJ,CAAC;YACD,OAAO,uBAAuB,CAAC;gBAC9B,cAAc,EAAE,IAAI;gBACpB,gBAAgB,EAAE,IAAI;gBACtB,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,UAAU,EAAE,CAAC;gBACb,MAAM,EACL,UAAU,KAAK,OAAO;oBACrB,CAAC,CAAC,0DAA0D;oBAC5D,CAAC,CAAC,yBAAyB;aAC7B,CAAC,CAAC;QACJ,CAAC;QACD,eAAe,CAAC,KAAuB;YACtC,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACrD,IAAI,CAAC,OAAO;gBAAE,OAAO,oBAAoB,EAAE,CAAC;YAC5C,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;YAC1B,MAAM,UAAU,GAAG,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACxD,MAAM,WAAW,GAAG,0BAA0B,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;YAClE,MAAM,cAAc,GAAG,uBAAuB,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;YACnE,MAAM,UAAU,GACf,CAAC,CAAC,WAAW,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC;gBACzC,CAAC,CAAC,yBAAyB,CACzB,KAAK,EACL,UAAU,EACV,KAAK,EACL,cAAc,EACd,OAAO,CAAC,0BAA0B,CAClC;gBACF,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;YAClB,MAAM,eAAe,GACpB,qBAAqB;gBACrB,UAAU;gBACV,UAAU,CAAC,OAAO,KAAK,YAAY;gBAClC,CAAC,CAAC,UAAU;gBACZ,CAAC,CAAC,IAAI,CAAC;YACT,MAAM,sBAAsB,GAC3B,UAAU,IAAI,UAAU,CAAC,OAAO,KAAK,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;YAEvE,uEAAuE;YACvE,wEAAwE;YACxE,cAAc;YACd,IAAI,eAAe,EAAE,CAAC;gBACrB,MAAM,eAAe,GAAG,mBAAmB,CAAC,cAAc,CAAC,CAAC;gBAC5D,IAAI,eAAe,EAAE,CAAC;oBACrB,OAAO,CAAC,kBAAkB,CAAC,GAAG,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;gBAClE,CAAC;gBACD,OAAO,uBAAuB,CAAC;oBAC9B,cAAc,EAAE,eAAe;oBAC/B,gBAAgB,EAAE,IAAI;oBACtB,YAAY,EAAE,OAAO,CAAC,YAAY;oBAClC,UAAU,EAAE,UAAU,CAAC,UAAU;oBACjC,MAAM,EAAE,UAAU,CAAC,MAAM;iBACzB,CAAC,CAAC;YACJ,CAAC;YAED,oEAAoE;YACpE,qEAAqE;YACrE,mEAAmE;YACnE,qEAAqE;YACrE,6DAA6D;YAC7D,gEAAgE;YAChE,oEAAoE;YACpE,IACC,WAAW;gBACX,CAAC,qBAAqB;oBACrB,CAAC,CAAC,CAAC,4BAA4B,CAAC,OAAO,EAAE,WAAW,EAAE,cAAc,CAAC;oBACrE,CAAC,CAAC,cAAc,CAAC,MAAM,KAAK,CAAC,CAAC,EAC9B,CAAC;gBACF,OAAO,uBAAuB,CAAC;oBAC9B,cAAc,EAAE,WAAW;oBAC3B,gBAAgB,EAAE,IAAI;oBACtB,YAAY,EAAE,OAAO,CAAC,YAAY;oBAClC,UAAU,EAAE,UAAU,CAAC,UAAU;oBACjC,MAAM,EAAE,UAAU,CAAC,MAAM;iBACzB,CAAC,CAAC;YACJ,CAAC;YAED,oEAAoE;YACpE,uEAAuE;YACvE,sEAAsE;YACtE,qEAAqE;YACrE,oEAAoE;YACpE,IAAI,yBAAyB,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE,CAAC;gBACxD,MAAM,eAAe,GAAG,mBAAmB,CAAC,cAAc,CAAC,CAAC;gBAC5D,MAAM,SAAS,GAAG,eAAe;oBAChC,CAAC,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC;oBAC3D,CAAC,CAAC,IAAI,CAAC;gBACR,OAAO,uBAAuB,CAAC;oBAC9B,cAAc,EAAE,SAAS;oBACzB,gBAAgB,EAAE,IAAI;oBACtB,YAAY,EAAE,OAAO,CAAC,YAAY;oBAClC,UAAU,EAAE,UAAU,CAAC,UAAU;oBACjC,MAAM,EAAE,SAAS;wBAChB,CAAC,CAAC,qCAAqC;wBACvC,CAAC,CAAC,kDAAkD;iBACrD,CAAC,CAAC;YACJ,CAAC;YAED,qEAAqE;YACrE,kEAAkE;YAClE,qDAAqD;YACrD,OAAO,uBAAuB,CAAC;gBAC9B,cAAc,EAAE,IAAI;gBACpB,gBAAgB,EACf,sBAAsB;oBACtB,mBAAmB,CAAC,OAAO,EAAE,cAAc,CAAC;gBAC7C,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,UAAU,EAAE,UAAU,CAAC,UAAU;gBACjC,MAAM,EAAE,UAAU,CAAC,MAAM;aACzB,CAAC,CAAC;QACJ,CAAC;KACD,CAAC;AACH,CAAC,CAAC","sourcesContent":["import { normalizeBoundaryEvent } from \"./boundary-normalizer.js\";\nimport { createHighlightDecision } from \"./highlight-policy.js\";\nimport { resolveMathBoundaryTarget } from \"./math-semantic-resolver.js\";\nimport { resolveProseBoundaryTarget } from \"./prose-boundary-resolver.js\";\nimport {\n\tclassifyMathHighlightCapability,\n\tcreateRenderedMathTargetResolver,\n\ttype MathHighlightCapability,\n\ttype RenderedMathTargetResolver,\n} from \"./rendered-math-target-resolver.js\";\nimport type {\n\tHighlightDecision,\n\tRenderableHighlightTarget,\n\tTTSBoundaryEvent,\n\tTTSHighlightChunk,\n\tTTSHighlightPlan,\n} from \"./types.js\";\n\nexport interface CreateTTSHighlightPlanArgs {\n\tchunks: TTSHighlightChunk[];\n\t// When false, math is never broken into per-token highlights: every equation\n\t// is painted as a single block — the same expression/region fallback used\n\t// whenever token alignment is not confident. Prose word tracking is\n\t// unaffected. Defaults to true.\n\tmathTokenHighlighting?: boolean;\n}\n\ninterface PlannedChunk {\n\tchunk: TTSHighlightChunk;\n\tregionTarget: RenderableHighlightTarget | null;\n\trenderedMathTargetResolver: RenderedMathTargetResolver;\n\t// Capability decided once per equation (keyed by its source math element).\n\t// Sticky for the whole chunk so the same equation never switches between\n\t// token tracking and whole-expression highlighting mid-read.\n\tmathCapabilityByElement: Map<Element, MathHighlightCapability>;\n\t// The last precise token painted for each token-mode equation. Lets a gap\n\t// (SSML break / pause / unmappable boundary) hold the last token instead of\n\t// flashing the whole expression.\n\theldTokenByElement: Map<Element, RenderableHighlightTarget>;\n}\n\nconst regionTargetFor = (\n\tchunk: TTSHighlightChunk,\n): RenderableHighlightTarget | null =>\n\tchunk.regionRange\n\t\t? {\n\t\t\t\ttype: \"range\",\n\t\t\t\tquality: \"region\",\n\t\t\t\trange: chunk.regionRange,\n\t\t\t}\n\t\t: chunk.regionElement\n\t\t\t? {\n\t\t\t\t\ttype: \"element\",\n\t\t\t\t\tquality: \"region\",\n\t\t\t\t\telement: chunk.regionElement,\n\t\t\t\t}\n\t\t\t: null;\n\nconst expressionTargetFor = (\n\tplanned: PlannedChunk,\n\tcandidates: TTSHighlightChunk[\"mathAlignments\"] = planned.chunk\n\t\t.mathAlignments,\n): RenderableHighlightTarget | null => {\n\tif (candidates.length !== 1) return null;\n\tconst fallback = candidates[0]?.alignment.result.fallbackTarget;\n\treturn fallback ? planned.renderedMathTargetResolver(fallback) : null;\n};\n\nconst mathCandidatesForTarget = (\n\ttarget: RenderableHighlightTarget | null,\n\tchunk: TTSHighlightChunk,\n): TTSHighlightChunk[\"mathAlignments\"] => {\n\tif (!target) return chunk.mathAlignments;\n\treturn chunk.mathAlignments.filter(({ element }) => {\n\t\ttry {\n\t\t\tif (target.type === \"range\") return target.range.intersectsNode(element);\n\t\t\tif (target.type === \"element\") {\n\t\t\t\treturn target.element === element || target.element.contains(element);\n\t\t\t}\n\t\t\treturn target.node.parentElement?.contains(element) || false;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t});\n};\n\nconst missingChunkDecision = (): HighlightDecision => ({\n\tactiveTarget: null,\n\tregionTarget: null,\n\tquality: \"region\",\n\tconfidence: 0,\n\treason: \"boundary chunk id was not found\",\n});\n\nconst soleEquationElement = (\n\tcandidates: TTSHighlightChunk[\"mathAlignments\"],\n): Element | null => (candidates.length === 1 ? candidates[0].element : null);\n\nconst everyCandidateIsTokenMode = (\n\tplanned: PlannedChunk,\n\tcandidates: TTSHighlightChunk[\"mathAlignments\"],\n): boolean =>\n\tcandidates.length > 0 &&\n\tcandidates.every(\n\t\t({ element }) => planned.mathCapabilityByElement.get(element) === \"token\",\n\t);\n\nconst TEXT_NODE = 3;\n\n// Safety policy for token-mode equations. A prose \"range\" target comes from the\n// catalog span aligner, whose spoken→visible mapping inside an equation is\n// heuristic and not trustworthy at glyph granularity. A multi-node range fully\n// inside a token-mode equation is therefore rejected here in favor of holding\n// the last token — painting it would be a likely false positive, and \"false\n// positives are worse than coarse highlighting.\" A single-text-node prose range\n// is kept: it underlines one token cleanly and usefully fills in any token the\n// math resolver missed. (The highlight coordinator no longer escalates word\n// ranges to the whole <math> / <mjx-container>, so this is now a precision\n// policy rather than flash-suppression — see HighlightCoordinator\n// WORD_FALLBACK_SELECTOR.)\nconst wouldEscalateInsideTokenMath = (\n\tplanned: PlannedChunk,\n\ttarget: RenderableHighlightTarget | null,\n\tcandidates: TTSHighlightChunk[\"mathAlignments\"],\n): boolean => {\n\tif (!target || target.type !== \"range\") return false;\n\tconst { startContainer, endContainer } = target.range;\n\tconst singleTextNode =\n\t\tstartContainer === endContainer && startContainer.nodeType === TEXT_NODE;\n\tif (singleTextNode) return false;\n\treturn candidates.some(\n\t\t({ element }) =>\n\t\t\tplanned.mathCapabilityByElement.get(element) === \"token\" &&\n\t\t\telement.contains(startContainer) &&\n\t\t\telement.contains(endContainer),\n\t);\n};\n\nconst createPlannedChunk = (\n\tchunk: TTSHighlightChunk,\n\tmathTokenHighlighting: boolean,\n): PlannedChunk => ({\n\tchunk,\n\tregionTarget: regionTargetFor(chunk),\n\trenderedMathTargetResolver: createRenderedMathTargetResolver(),\n\tmathCapabilityByElement: new Map(\n\t\tchunk.mathAlignments.map(({ element }) => [\n\t\t\telement,\n\t\t\t// Forcing \"expression\" when per-token math highlighting is disabled\n\t\t\t// routes every equation through the sticky whole-block path, so the\n\t\t\t// formula is highlighted as one unit instead of glyph by glyph.\n\t\t\tmathTokenHighlighting\n\t\t\t\t? classifyMathHighlightCapability(element)\n\t\t\t\t: \"expression\",\n\t\t]),\n\t),\n\theldTokenByElement: new Map(),\n});\n\nexport const createTTSHighlightPlan = (\n\targs: CreateTTSHighlightPlanArgs,\n): TTSHighlightPlan => {\n\tconst mathTokenHighlighting = args.mathTokenHighlighting !== false;\n\tconst plannedChunksById = new Map(\n\t\targs.chunks.map((chunk) => [\n\t\t\tchunk.id,\n\t\t\tcreatePlannedChunk(chunk, mathTokenHighlighting),\n\t\t]),\n\t);\n\treturn {\n\t\tchunks: args.chunks,\n\t\tresolveInitial(chunkId: string): HighlightDecision {\n\t\t\tconst planned = plannedChunksById.get(chunkId);\n\t\t\tif (!planned) return missingChunkDecision();\n\t\t\tconst alignments = planned.chunk.mathAlignments;\n\t\t\tconst soleElement = alignments.length === 1 ? alignments[0].element : null;\n\t\t\tconst capability = soleElement\n\t\t\t\t? planned.mathCapabilityByElement.get(soleElement)\n\t\t\t\t: null;\n\t\t\t// Sticky mode. Only an equation we already know cannot be tracked per\n\t\t\t// token gets the whole-expression paint up front. A token-mode equation\n\t\t\t// stays on the region layer until its first token resolves, so the full\n\t\t\t// equation never flashes before (or between) tokens.\n\t\t\tif (capability === \"expression\") {\n\t\t\t\treturn createHighlightDecision({\n\t\t\t\t\tsemanticTarget: null,\n\t\t\t\t\texpressionTarget: expressionTargetFor(planned),\n\t\t\t\t\tregionTarget: planned.regionTarget,\n\t\t\t\t\tconfidence: 0.5,\n\t\t\t\t\treason: \"initial expression (sticky expression mode)\",\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn createHighlightDecision({\n\t\t\t\tsemanticTarget: null,\n\t\t\t\texpressionTarget: null,\n\t\t\t\tregionTarget: planned.regionTarget,\n\t\t\t\tconfidence: 0,\n\t\t\t\treason:\n\t\t\t\t\tcapability === \"token\"\n\t\t\t\t\t\t? \"initial region (sticky token mode, awaiting first token)\"\n\t\t\t\t\t\t: \"initial region fallback\",\n\t\t\t});\n\t\t},\n\t\tresolveBoundary(event: TTSBoundaryEvent): HighlightDecision {\n\t\t\tconst planned = plannedChunksById.get(event.chunkId);\n\t\t\tif (!planned) return missingChunkDecision();\n\t\t\tconst { chunk } = planned;\n\t\t\tconst normalized = normalizeBoundaryEvent(chunk, event);\n\t\t\tconst proseTarget = resolveProseBoundaryTarget(chunk, normalized);\n\t\t\tconst mathCandidates = mathCandidatesForTarget(proseTarget, chunk);\n\t\t\tconst mathTarget =\n\t\t\t\t(!proseTarget || mathCandidates.length > 0\n\t\t\t\t\t? resolveMathBoundaryTarget(\n\t\t\t\t\t\t\tchunk,\n\t\t\t\t\t\t\tnormalized,\n\t\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\tmathCandidates,\n\t\t\t\t\t\t\tplanned.renderedMathTargetResolver,\n\t\t\t\t\t\t)\n\t\t\t\t\t: null) || null;\n\t\t\tconst mathTokenTarget =\n\t\t\t\tmathTokenHighlighting &&\n\t\t\t\tmathTarget &&\n\t\t\t\tmathTarget.quality !== \"expression\"\n\t\t\t\t\t? mathTarget\n\t\t\t\t\t: null;\n\t\t\tconst mathExpressionFallback =\n\t\t\t\tmathTarget && mathTarget.quality === \"expression\" ? mathTarget : null;\n\n\t\t\t// (1) A precise token always wins, and becomes the held target for its\n\t\t\t// equation so a later gap can keep it lit instead of flashing the whole\n\t\t\t// expression.\n\t\t\tif (mathTokenTarget) {\n\t\t\t\tconst equationElement = soleEquationElement(mathCandidates);\n\t\t\t\tif (equationElement) {\n\t\t\t\t\tplanned.heldTokenByElement.set(equationElement, mathTokenTarget);\n\t\t\t\t}\n\t\t\t\treturn createHighlightDecision({\n\t\t\t\t\tsemanticTarget: mathTokenTarget,\n\t\t\t\t\texpressionTarget: null,\n\t\t\t\t\tregionTarget: planned.regionTarget,\n\t\t\t\t\tconfidence: normalized.confidence,\n\t\t\t\t\treason: normalized.reason,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// (2) A spoken word that maps to visible prose. The prose anchor is\n\t\t\t// precise; a coarse math expression fallback must never override it.\n\t\t\t// Exception: a multi-node prose range inside a token-mode equation\n\t\t\t// would escalate to the whole expression, so it is rejected here and\n\t\t\t// falls through to the token hold below. When per-token math\n\t\t\t// highlighting is disabled, any prose range that lands inside a\n\t\t\t// formula is likewise rejected so the formula stays a single block.\n\t\t\tif (\n\t\t\t\tproseTarget &&\n\t\t\t\t(mathTokenHighlighting\n\t\t\t\t\t? !wouldEscalateInsideTokenMath(planned, proseTarget, mathCandidates)\n\t\t\t\t\t: mathCandidates.length === 0)\n\t\t\t) {\n\t\t\t\treturn createHighlightDecision({\n\t\t\t\t\tsemanticTarget: proseTarget,\n\t\t\t\t\texpressionTarget: null,\n\t\t\t\t\tregionTarget: planned.regionTarget,\n\t\t\t\t\tconfidence: normalized.confidence,\n\t\t\t\t\treason: normalized.reason,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// (3) A boundary inside a token-mode equation that did not map to a\n\t\t\t// token — an SSML break, a pause, or a gap between spoken tokens. Hold\n\t\t\t// the last token instead of flashing the whole expression; before the\n\t\t\t// first token there is nothing to hold, so fall to the region layer.\n\t\t\t// The whole expression is deliberately never painted in token mode.\n\t\t\tif (everyCandidateIsTokenMode(planned, mathCandidates)) {\n\t\t\t\tconst equationElement = soleEquationElement(mathCandidates);\n\t\t\t\tconst heldToken = equationElement\n\t\t\t\t\t? (planned.heldTokenByElement.get(equationElement) ?? null)\n\t\t\t\t\t: null;\n\t\t\t\treturn createHighlightDecision({\n\t\t\t\t\tsemanticTarget: heldToken,\n\t\t\t\t\texpressionTarget: null,\n\t\t\t\t\tregionTarget: planned.regionTarget,\n\t\t\t\t\tconfidence: normalized.confidence,\n\t\t\t\t\treason: heldToken\n\t\t\t\t\t\t? \"hold last token (sticky token mode)\"\n\t\t\t\t\t\t: \"region (sticky token mode, awaiting first token)\",\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// (4) An expression-mode (or mixed/ambiguous) math region: paint the\n\t\t\t// whole equation. Stable for the equation's full duration because\n\t\t\t// tokens never resolve here, so there is no flicker.\n\t\t\treturn createHighlightDecision({\n\t\t\t\tsemanticTarget: null,\n\t\t\t\texpressionTarget:\n\t\t\t\t\tmathExpressionFallback ??\n\t\t\t\t\texpressionTargetFor(planned, mathCandidates),\n\t\t\t\tregionTarget: planned.regionTarget,\n\t\t\t\tconfidence: normalized.confidence,\n\t\t\t\treason: normalized.reason,\n\t\t\t});\n\t\t},\n\t};\n};\n"]}
@@ -0,0 +1,2 @@
1
+ import type { HighlightDecision, HighlightDecisionInput } from "./types.js";
2
+ export declare const createHighlightDecision: (input: HighlightDecisionInput) => HighlightDecision;
@@ -0,0 +1,32 @@
1
+ export const createHighlightDecision = (input) => {
2
+ if (input.semanticTarget) {
3
+ return {
4
+ activeTarget: input.semanticTarget,
5
+ regionTarget: input.regionTarget,
6
+ quality: input.semanticTarget.quality === "exact-word"
7
+ ? "exact-word"
8
+ : input.semanticTarget.quality === "expression"
9
+ ? "expression"
10
+ : "semantic-token",
11
+ confidence: input.confidence,
12
+ reason: input.reason,
13
+ };
14
+ }
15
+ if (input.expressionTarget) {
16
+ return {
17
+ activeTarget: input.expressionTarget,
18
+ regionTarget: input.regionTarget,
19
+ quality: "expression",
20
+ confidence: input.confidence,
21
+ reason: input.reason,
22
+ };
23
+ }
24
+ return {
25
+ activeTarget: null,
26
+ regionTarget: input.regionTarget,
27
+ quality: "region",
28
+ confidence: input.confidence,
29
+ reason: input.reason,
30
+ };
31
+ };
32
+ //# sourceMappingURL=highlight-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"highlight-policy.js","sourceRoot":"","sources":["../../../../src/services/tts/highlight-pipeline/highlight-policy.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACtC,KAA6B,EACT,EAAE;IACtB,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;QAC1B,OAAO;YACN,YAAY,EAAE,KAAK,CAAC,cAAc;YAClC,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,OAAO,EACN,KAAK,CAAC,cAAc,CAAC,OAAO,KAAK,YAAY;gBAC5C,CAAC,CAAC,YAAY;gBACd,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,KAAK,YAAY;oBAC9C,CAAC,CAAC,YAAY;oBACd,CAAC,CAAC,gBAAgB;YACrB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;SACpB,CAAC;IACH,CAAC;IACD,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;QAC5B,OAAO;YACN,YAAY,EAAE,KAAK,CAAC,gBAAgB;YACpC,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,OAAO,EAAE,YAAY;YACrB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;SACpB,CAAC;IACH,CAAC;IACD,OAAO;QACN,YAAY,EAAE,IAAI;QAClB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,OAAO,EAAE,QAAQ;QACjB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;KACpB,CAAC;AACH,CAAC,CAAC","sourcesContent":["import type { HighlightDecision, HighlightDecisionInput } from \"./types.js\";\n\nexport const createHighlightDecision = (\n\tinput: HighlightDecisionInput,\n): HighlightDecision => {\n\tif (input.semanticTarget) {\n\t\treturn {\n\t\t\tactiveTarget: input.semanticTarget,\n\t\t\tregionTarget: input.regionTarget,\n\t\t\tquality:\n\t\t\t\tinput.semanticTarget.quality === \"exact-word\"\n\t\t\t\t\t? \"exact-word\"\n\t\t\t\t\t: input.semanticTarget.quality === \"expression\"\n\t\t\t\t\t\t? \"expression\"\n\t\t\t\t\t\t: \"semantic-token\",\n\t\t\tconfidence: input.confidence,\n\t\t\treason: input.reason,\n\t\t};\n\t}\n\tif (input.expressionTarget) {\n\t\treturn {\n\t\t\tactiveTarget: input.expressionTarget,\n\t\t\tregionTarget: input.regionTarget,\n\t\t\tquality: \"expression\",\n\t\t\tconfidence: input.confidence,\n\t\t\treason: input.reason,\n\t\t};\n\t}\n\treturn {\n\t\tactiveTarget: null,\n\t\tregionTarget: input.regionTarget,\n\t\tquality: \"region\",\n\t\tconfidence: input.confidence,\n\t\treason: input.reason,\n\t};\n};\n"]}
@@ -0,0 +1,9 @@
1
+ export * from "./boundary-normalizer.js";
2
+ export * from "./create-highlight-plan.js";
3
+ export * from "./highlight-policy.js";
4
+ export * from "./math-semantic-resolver.js";
5
+ export * from "./normalize-speech-chunks.js";
6
+ export * from "./prose-boundary-resolver.js";
7
+ export * from "./readable-region.js";
8
+ export * from "./rendered-math-target-resolver.js";
9
+ export type * from "./types.js";
@@ -0,0 +1,9 @@
1
+ export * from "./boundary-normalizer.js";
2
+ export * from "./create-highlight-plan.js";
3
+ export * from "./highlight-policy.js";
4
+ export * from "./math-semantic-resolver.js";
5
+ export * from "./normalize-speech-chunks.js";
6
+ export * from "./prose-boundary-resolver.js";
7
+ export * from "./readable-region.js";
8
+ export * from "./rendered-math-target-resolver.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/services/tts/highlight-pipeline/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,oCAAoC,CAAC","sourcesContent":["export * from \"./boundary-normalizer.js\";\nexport * from \"./create-highlight-plan.js\";\nexport * from \"./highlight-policy.js\";\nexport * from \"./math-semantic-resolver.js\";\nexport * from \"./normalize-speech-chunks.js\";\nexport * from \"./prose-boundary-resolver.js\";\nexport * from \"./readable-region.js\";\nexport * from \"./rendered-math-target-resolver.js\";\nexport type * from \"./types.js\";\n"]}
@@ -0,0 +1,3 @@
1
+ import { type RenderedMathTargetResolver } from "./rendered-math-target-resolver.js";
2
+ import type { NormalizedBoundaryEvent, RenderableHighlightTarget, TTSBoundaryEvent, TTSHighlightChunk } from "./types.js";
3
+ export declare const resolveMathBoundaryTarget: (chunk: TTSHighlightChunk, boundary: NormalizedBoundaryEvent, event: TTSBoundaryEvent, candidates?: TTSHighlightChunk["mathAlignments"], renderedResolver?: RenderedMathTargetResolver) => RenderableHighlightTarget | null;
@@ -0,0 +1,32 @@
1
+ import { resolveMatchedHighlightTargetForSpokenBoundary, resolveUniqueMathTargetForBoundaryWord, } from "../math-alignment/index.js";
2
+ import { resolveRenderedMathTarget, } from "./rendered-math-target-resolver.js";
3
+ export const resolveMathBoundaryTarget = (chunk, boundary, event, candidates = chunk.mathAlignments, renderedResolver = resolveRenderedMathTarget) => {
4
+ if (boundary.chunkSpokenStart === null || boundary.chunkSpokenEnd === null) {
5
+ return null;
6
+ }
7
+ // Prefer a precise per-token target across all candidate equations before
8
+ // settling for a coarse expression fallback: scan every candidate for a
9
+ // semantic-token first, remembering the first expression-quality hit only as
10
+ // a last resort.
11
+ let expressionFallback = null;
12
+ for (const mathAlignment of candidates) {
13
+ const target = resolveMatchedHighlightTargetForSpokenBoundary(mathAlignment.alignment, {
14
+ position: boundary.chunkSpokenStart,
15
+ length: Math.max(1, boundary.chunkSpokenEnd - boundary.chunkSpokenStart),
16
+ boundaryWord: event.word,
17
+ }) ||
18
+ resolveUniqueMathTargetForBoundaryWord(mathAlignment.alignment, {
19
+ boundaryWord: event.word,
20
+ });
21
+ if (!target)
22
+ continue;
23
+ const rendered = renderedResolver(target);
24
+ if (!rendered)
25
+ continue;
26
+ if (rendered.quality === "semantic-token")
27
+ return rendered;
28
+ expressionFallback ??= rendered;
29
+ }
30
+ return expressionFallback;
31
+ };
32
+ //# sourceMappingURL=math-semantic-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"math-semantic-resolver.js","sourceRoot":"","sources":["../../../../src/services/tts/highlight-pipeline/math-semantic-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,8CAA8C,EAC9C,sCAAsC,GACtC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACN,yBAAyB,GAEzB,MAAM,oCAAoC,CAAC;AAQ5C,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACxC,KAAwB,EACxB,QAAiC,EACjC,KAAuB,EACvB,aAAkD,KAAK,CAAC,cAAc,EACtE,mBAA+C,yBAAyB,EACrC,EAAE;IACrC,IAAI,QAAQ,CAAC,gBAAgB,KAAK,IAAI,IAAI,QAAQ,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;QAC5E,OAAO,IAAI,CAAC;IACb,CAAC;IACD,0EAA0E;IAC1E,wEAAwE;IACxE,6EAA6E;IAC7E,iBAAiB;IACjB,IAAI,kBAAkB,GAAqC,IAAI,CAAC;IAChE,KAAK,MAAM,aAAa,IAAI,UAAU,EAAE,CAAC;QACxC,MAAM,MAAM,GACX,8CAA8C,CAAC,aAAa,CAAC,SAAS,EAAE;YACvE,QAAQ,EAAE,QAAQ,CAAC,gBAAgB;YACnC,MAAM,EAAE,IAAI,CAAC,GAAG,CACf,CAAC,EACD,QAAQ,CAAC,cAAc,GAAG,QAAQ,CAAC,gBAAgB,CACnD;YACD,YAAY,EAAE,KAAK,CAAC,IAAI;SACxB,CAAC;YACF,sCAAsC,CAAC,aAAa,CAAC,SAAS,EAAE;gBAC/D,YAAY,EAAE,KAAK,CAAC,IAAI;aACxB,CAAC,CAAC;QACJ,IAAI,CAAC,MAAM;YAAE,SAAS;QACtB,MAAM,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,CAAC,QAAQ;YAAE,SAAS;QACxB,IAAI,QAAQ,CAAC,OAAO,KAAK,gBAAgB;YAAE,OAAO,QAAQ,CAAC;QAC3D,kBAAkB,KAAK,QAAQ,CAAC;IACjC,CAAC;IACD,OAAO,kBAAkB,CAAC;AAC3B,CAAC,CAAC","sourcesContent":["import {\n\tresolveMatchedHighlightTargetForSpokenBoundary,\n\tresolveUniqueMathTargetForBoundaryWord,\n} from \"../math-alignment/index.js\";\nimport {\n\tresolveRenderedMathTarget,\n\ttype RenderedMathTargetResolver,\n} from \"./rendered-math-target-resolver.js\";\nimport type {\n\tNormalizedBoundaryEvent,\n\tRenderableHighlightTarget,\n\tTTSBoundaryEvent,\n\tTTSHighlightChunk,\n} from \"./types.js\";\n\nexport const resolveMathBoundaryTarget = (\n\tchunk: TTSHighlightChunk,\n\tboundary: NormalizedBoundaryEvent,\n\tevent: TTSBoundaryEvent,\n\tcandidates: TTSHighlightChunk[\"mathAlignments\"] = chunk.mathAlignments,\n\trenderedResolver: RenderedMathTargetResolver = resolveRenderedMathTarget,\n): RenderableHighlightTarget | null => {\n\tif (boundary.chunkSpokenStart === null || boundary.chunkSpokenEnd === null) {\n\t\treturn null;\n\t}\n\t// Prefer a precise per-token target across all candidate equations before\n\t// settling for a coarse expression fallback: scan every candidate for a\n\t// semantic-token first, remembering the first expression-quality hit only as\n\t// a last resort.\n\tlet expressionFallback: RenderableHighlightTarget | null = null;\n\tfor (const mathAlignment of candidates) {\n\t\tconst target =\n\t\t\tresolveMatchedHighlightTargetForSpokenBoundary(mathAlignment.alignment, {\n\t\t\t\tposition: boundary.chunkSpokenStart,\n\t\t\t\tlength: Math.max(\n\t\t\t\t\t1,\n\t\t\t\t\tboundary.chunkSpokenEnd - boundary.chunkSpokenStart,\n\t\t\t\t),\n\t\t\t\tboundaryWord: event.word,\n\t\t\t}) ||\n\t\t\tresolveUniqueMathTargetForBoundaryWord(mathAlignment.alignment, {\n\t\t\t\tboundaryWord: event.word,\n\t\t\t});\n\t\tif (!target) continue;\n\t\tconst rendered = renderedResolver(target);\n\t\tif (!rendered) continue;\n\t\tif (rendered.quality === \"semantic-token\") return rendered;\n\t\texpressionFallback ??= rendered;\n\t}\n\treturn expressionFallback;\n};\n"]}
@@ -0,0 +1,29 @@
1
+ import type { CatalogSpanAlignment } from "../catalog-span-alignment.js";
2
+ import type { MathAwareAlignment } from "../math-alignment/index.js";
3
+ import type { NormalizedTextMap } from "../text-processing.js";
4
+ import type { TTSHighlightChunk } from "./types.js";
5
+ export interface SpeechCompositionChunkInput {
6
+ speechText: string;
7
+ visibleText: string;
8
+ sourceElement: Element | null;
9
+ regionElement?: Element | null;
10
+ regionRange?: Range;
11
+ alignment?: CatalogSpanAlignment;
12
+ mathAlignment?: MathAwareAlignment;
13
+ mathAlignments?: Array<{
14
+ element: Element;
15
+ alignment: MathAwareAlignment;
16
+ }>;
17
+ visibleMap?: NormalizedTextMap;
18
+ }
19
+ export interface NormalizeSpeechChunksArgs {
20
+ contentRoot: Element;
21
+ chunks?: SpeechCompositionChunkInput[];
22
+ speechText?: string;
23
+ visibleText?: string;
24
+ sourceElement?: Element | null;
25
+ regionElement?: Element | null;
26
+ regionRange?: Range;
27
+ visibleMap?: NormalizedTextMap;
28
+ }
29
+ export declare const normalizeSpeechChunks: (args: NormalizeSpeechChunksArgs) => TTSHighlightChunk[];
@@ -0,0 +1,44 @@
1
+ const chunkOffsetSpaceFor = (chunk) => {
2
+ if (chunk.alignment?.boundaryOffsetMode)
3
+ return chunk.alignment.boundaryOffsetMode;
4
+ const mathOffsetSpace = chunk.mathAlignment?.speech.boundaryOffsetSpace;
5
+ if (mathOffsetSpace)
6
+ return mathOffsetSpace;
7
+ return "plain-spoken-text";
8
+ };
9
+ const mathAlignmentsFor = (chunk) => {
10
+ if (chunk.mathAlignments?.length)
11
+ return chunk.mathAlignments;
12
+ if (chunk.mathAlignment && chunk.sourceElement) {
13
+ return [{ element: chunk.sourceElement, alignment: chunk.mathAlignment }];
14
+ }
15
+ return [];
16
+ };
17
+ export const normalizeSpeechChunks = (args) => {
18
+ const sourceChunks = args.chunks && args.chunks.length
19
+ ? args.chunks
20
+ : [
21
+ {
22
+ speechText: args.speechText || "",
23
+ visibleText: args.visibleText || args.speechText || "",
24
+ sourceElement: args.sourceElement || args.contentRoot,
25
+ regionElement: args.regionElement || args.sourceElement || args.contentRoot,
26
+ regionRange: args.regionRange,
27
+ visibleMap: args.visibleMap,
28
+ },
29
+ ];
30
+ return sourceChunks.map((chunk, index) => ({
31
+ id: `tts-chunk-${index}`,
32
+ speechText: chunk.speechText,
33
+ visibleText: chunk.visibleText,
34
+ sourceElement: chunk.sourceElement,
35
+ contentRoot: args.contentRoot,
36
+ regionElement: chunk.regionElement ?? chunk.sourceElement,
37
+ regionRange: chunk.regionRange,
38
+ visibleMap: chunk.visibleMap,
39
+ catalogAlignment: chunk.alignment,
40
+ mathAlignments: mathAlignmentsFor(chunk),
41
+ offsetSpace: chunkOffsetSpaceFor(chunk),
42
+ }));
43
+ };
44
+ //# sourceMappingURL=normalize-speech-chunks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-speech-chunks.js","sourceRoot":"","sources":["../../../../src/services/tts/highlight-pipeline/normalize-speech-chunks.ts"],"names":[],"mappings":"AA4BA,MAAM,mBAAmB,GAAG,CAC3B,KAAkC,EACf,EAAE;IACrB,IAAI,KAAK,CAAC,SAAS,EAAE,kBAAkB;QACtC,OAAO,KAAK,CAAC,SAAS,CAAC,kBAAkB,CAAC;IAC3C,MAAM,eAAe,GAAG,KAAK,CAAC,aAAa,EAAE,MAAM,CAAC,mBAAmB,CAAC;IACxE,IAAI,eAAe;QAAE,OAAO,eAAe,CAAC;IAC5C,OAAO,mBAAmB,CAAC;AAC5B,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,CACzB,KAAkC,EAC2B,EAAE;IAC/D,IAAI,KAAK,CAAC,cAAc,EAAE,MAAM;QAAE,OAAO,KAAK,CAAC,cAAc,CAAC;IAC9D,IAAI,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;QAChD,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,aAAa,EAAE,SAAS,EAAE,KAAK,CAAC,aAAa,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,EAAE,CAAC;AACX,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACpC,IAA+B,EACT,EAAE;IACxB,MAAM,YAAY,GACjB,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM;QAChC,CAAC,CAAC,IAAI,CAAC,MAAM;QACb,CAAC,CAAC;YACA;gBACC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,EAAE;gBACjC,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,UAAU,IAAI,EAAE;gBACtD,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,WAAW;gBACrD,aAAa,EACZ,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,WAAW;gBAC7D,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,UAAU,EAAE,IAAI,CAAC,UAAU;aAC3B;SACD,CAAC;IACL,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QAC1C,EAAE,EAAE,aAAa,KAAK,EAAE;QACxB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,aAAa,EAAE,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,aAAa;QACzD,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,gBAAgB,EAAE,KAAK,CAAC,SAAS;QACjC,cAAc,EAAE,iBAAiB,CAAC,KAAK,CAAC;QACxC,WAAW,EAAE,mBAAmB,CAAC,KAAK,CAAC;KACvC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC","sourcesContent":["import type { CatalogSpanAlignment } from \"../catalog-span-alignment.js\";\nimport type { MathAwareAlignment } from \"../math-alignment/index.js\";\nimport type { NormalizedTextMap } from \"../text-processing.js\";\nimport type { ChunkOffsetSpace, TTSHighlightChunk } from \"./types.js\";\n\nexport interface SpeechCompositionChunkInput {\n\tspeechText: string;\n\tvisibleText: string;\n\tsourceElement: Element | null;\n\tregionElement?: Element | null;\n\tregionRange?: Range;\n\talignment?: CatalogSpanAlignment;\n\tmathAlignment?: MathAwareAlignment;\n\tmathAlignments?: Array<{ element: Element; alignment: MathAwareAlignment }>;\n\tvisibleMap?: NormalizedTextMap;\n}\n\nexport interface NormalizeSpeechChunksArgs {\n\tcontentRoot: Element;\n\tchunks?: SpeechCompositionChunkInput[];\n\tspeechText?: string;\n\tvisibleText?: string;\n\tsourceElement?: Element | null;\n\tregionElement?: Element | null;\n\tregionRange?: Range;\n\tvisibleMap?: NormalizedTextMap;\n}\n\nconst chunkOffsetSpaceFor = (\n\tchunk: SpeechCompositionChunkInput,\n): ChunkOffsetSpace => {\n\tif (chunk.alignment?.boundaryOffsetMode)\n\t\treturn chunk.alignment.boundaryOffsetMode;\n\tconst mathOffsetSpace = chunk.mathAlignment?.speech.boundaryOffsetSpace;\n\tif (mathOffsetSpace) return mathOffsetSpace;\n\treturn \"plain-spoken-text\";\n};\n\nconst mathAlignmentsFor = (\n\tchunk: SpeechCompositionChunkInput,\n): Array<{ element: Element; alignment: MathAwareAlignment }> => {\n\tif (chunk.mathAlignments?.length) return chunk.mathAlignments;\n\tif (chunk.mathAlignment && chunk.sourceElement) {\n\t\treturn [{ element: chunk.sourceElement, alignment: chunk.mathAlignment }];\n\t}\n\treturn [];\n};\n\nexport const normalizeSpeechChunks = (\n\targs: NormalizeSpeechChunksArgs,\n): TTSHighlightChunk[] => {\n\tconst sourceChunks =\n\t\targs.chunks && args.chunks.length\n\t\t\t? args.chunks\n\t\t\t: [\n\t\t\t\t\t{\n\t\t\t\t\t\tspeechText: args.speechText || \"\",\n\t\t\t\t\t\tvisibleText: args.visibleText || args.speechText || \"\",\n\t\t\t\t\t\tsourceElement: args.sourceElement || args.contentRoot,\n\t\t\t\t\t\tregionElement:\n\t\t\t\t\t\t\targs.regionElement || args.sourceElement || args.contentRoot,\n\t\t\t\t\t\tregionRange: args.regionRange,\n\t\t\t\t\t\tvisibleMap: args.visibleMap,\n\t\t\t\t\t},\n\t\t\t\t];\n\treturn sourceChunks.map((chunk, index) => ({\n\t\tid: `tts-chunk-${index}`,\n\t\tspeechText: chunk.speechText,\n\t\tvisibleText: chunk.visibleText,\n\t\tsourceElement: chunk.sourceElement,\n\t\tcontentRoot: args.contentRoot,\n\t\tregionElement: chunk.regionElement ?? chunk.sourceElement,\n\t\tregionRange: chunk.regionRange,\n\t\tvisibleMap: chunk.visibleMap,\n\t\tcatalogAlignment: chunk.alignment,\n\t\tmathAlignments: mathAlignmentsFor(chunk),\n\t\toffsetSpace: chunkOffsetSpaceFor(chunk),\n\t}));\n};\n"]}
@@ -0,0 +1,2 @@
1
+ import type { NormalizedBoundaryEvent, RenderableHighlightTarget, TTSHighlightChunk } from "./types.js";
2
+ export declare const resolveProseBoundaryTarget: (chunk: TTSHighlightChunk, boundary: NormalizedBoundaryEvent) => RenderableHighlightTarget | null;
@@ -0,0 +1,20 @@
1
+ import { resolveVisibleSpanForBoundary } from "../catalog-span-alignment.js";
2
+ import { createRangeFromVisibleMap } from "./visible-map-range.js";
3
+ export const resolveProseBoundaryTarget = (chunk, boundary) => {
4
+ if (!chunk.catalogAlignment || boundary.chunkSpokenStart === null)
5
+ return null;
6
+ const visibleSpan = resolveVisibleSpanForBoundary(chunk.catalogAlignment, boundary.chunkSpokenStart);
7
+ if (!visibleSpan)
8
+ return null;
9
+ const range = createRangeFromVisibleMap(chunk.visibleMap, visibleSpan.start, visibleSpan.end);
10
+ if (!range)
11
+ return null;
12
+ return {
13
+ type: "range",
14
+ quality: chunk.catalogAlignment.playbackMode === "exact-word"
15
+ ? "exact-word"
16
+ : "semantic-token",
17
+ range,
18
+ };
19
+ };
20
+ //# sourceMappingURL=prose-boundary-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prose-boundary-resolver.js","sourceRoot":"","sources":["../../../../src/services/tts/highlight-pipeline/prose-boundary-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAC7E,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAOnE,MAAM,CAAC,MAAM,0BAA0B,GAAG,CACzC,KAAwB,EACxB,QAAiC,EACE,EAAE;IACrC,IAAI,CAAC,KAAK,CAAC,gBAAgB,IAAI,QAAQ,CAAC,gBAAgB,KAAK,IAAI;QAChE,OAAO,IAAI,CAAC;IACb,MAAM,WAAW,GAAG,6BAA6B,CAChD,KAAK,CAAC,gBAAgB,EACtB,QAAQ,CAAC,gBAAgB,CACzB,CAAC;IACF,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,KAAK,GAAG,yBAAyB,CACtC,KAAK,CAAC,UAAU,EAChB,WAAW,CAAC,KAAK,EACjB,WAAW,CAAC,GAAG,CACf,CAAC;IACF,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO;QACN,IAAI,EAAE,OAAO;QACb,OAAO,EACN,KAAK,CAAC,gBAAgB,CAAC,YAAY,KAAK,YAAY;YACnD,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,gBAAgB;QACpB,KAAK;KACL,CAAC;AACH,CAAC,CAAC","sourcesContent":["import { resolveVisibleSpanForBoundary } from \"../catalog-span-alignment.js\";\nimport { createRangeFromVisibleMap } from \"./visible-map-range.js\";\nimport type {\n\tNormalizedBoundaryEvent,\n\tRenderableHighlightTarget,\n\tTTSHighlightChunk,\n} from \"./types.js\";\n\nexport const resolveProseBoundaryTarget = (\n\tchunk: TTSHighlightChunk,\n\tboundary: NormalizedBoundaryEvent,\n): RenderableHighlightTarget | null => {\n\tif (!chunk.catalogAlignment || boundary.chunkSpokenStart === null)\n\t\treturn null;\n\tconst visibleSpan = resolveVisibleSpanForBoundary(\n\t\tchunk.catalogAlignment,\n\t\tboundary.chunkSpokenStart,\n\t);\n\tif (!visibleSpan) return null;\n\tconst range = createRangeFromVisibleMap(\n\t\tchunk.visibleMap,\n\t\tvisibleSpan.start,\n\t\tvisibleSpan.end,\n\t);\n\tif (!range) return null;\n\treturn {\n\t\ttype: \"range\",\n\t\tquality:\n\t\t\tchunk.catalogAlignment.playbackMode === \"exact-word\"\n\t\t\t\t? \"exact-word\"\n\t\t\t\t: \"semantic-token\",\n\t\trange,\n\t};\n};\n"]}
@@ -0,0 +1 @@
1
+ export declare const resolveReadableRegion: (sourceElement: Element, contentRoot: Element) => Element;