@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.
- package/README.md +54 -69
- package/dist/components/ItemToolBar.custom-element.js +479 -324
- package/dist/components/PieAssessmentToolkit.custom-element.js +19498 -3590
- package/dist/components/SectionToolBar.custom-element.js +761 -507
- package/dist/index.d.ts +3 -6
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/policy/core/ToolPolicyEngine.d.ts +1 -2
- package/dist/policy/core/ToolPolicyEngine.js +1 -2
- package/dist/policy/core/ToolPolicyEngine.js.map +1 -1
- package/dist/policy/core/pnp-policy-inputs.d.ts +2 -3
- package/dist/policy/core/pnp-policy-inputs.js +2 -3
- package/dist/policy/core/pnp-policy-inputs.js.map +1 -1
- package/dist/policy/core/provenance.d.ts +7 -34
- package/dist/policy/core/provenance.js +4 -21
- package/dist/policy/core/provenance.js.map +1 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +6 -19
- package/dist/policy/sources/PnpPolicySource.js +6 -19
- package/dist/policy/sources/PnpPolicySource.js.map +1 -1
- package/dist/runtime/SectionRuntimeEngine.d.ts +13 -28
- package/dist/runtime/SectionRuntimeEngine.js +13 -27
- package/dist/runtime/SectionRuntimeEngine.js.map +1 -1
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +2 -2
- package/dist/runtime/adapter/SectionEngineAdapter.js +2 -2
- package/dist/runtime/adapter/SectionEngineAdapter.js.map +1 -1
- package/dist/runtime/adapter/coordinator-bridge.d.ts +1 -1
- package/dist/runtime/adapter/coordinator-bridge.js +1 -1
- package/dist/runtime/adapter/coordinator-bridge.js.map +1 -1
- package/dist/runtime/adapter/dom-event-bridge.d.ts +1 -1
- package/dist/runtime/adapter/dom-event-bridge.js +1 -1
- package/dist/runtime/adapter/dom-event-bridge.js.map +1 -1
- package/dist/runtime/catalog-registration.d.ts +12 -0
- package/dist/runtime/catalog-registration.js +54 -0
- package/dist/runtime/catalog-registration.js.map +1 -0
- package/dist/runtime/core/engine-output.d.ts +2 -2
- package/dist/runtime/core/engine-output.js +2 -2
- package/dist/runtime/core/engine-output.js.map +1 -1
- package/dist/runtime/core/engine-readiness.d.ts +2 -2
- package/dist/runtime/core/engine-readiness.js.map +1 -1
- package/dist/runtime/core/engine-resolver.d.ts +13 -41
- package/dist/runtime/core/engine-resolver.js +19 -76
- package/dist/runtime/core/engine-resolver.js.map +1 -1
- package/dist/runtime/core/engine-transition.d.ts +1 -1
- package/dist/runtime/core/engine-transition.js +1 -1
- package/dist/runtime/core/engine-transition.js.map +1 -1
- package/dist/runtime/engine.d.ts +1 -1
- package/dist/runtime/engine.js.map +1 -1
- package/dist/runtime/section-runtime-engine-host-context.d.ts +22 -15
- package/dist/runtime/section-runtime-engine-host-context.js +10 -10
- package/dist/runtime/section-runtime-engine-host-context.js.map +1 -1
- package/dist/runtime/stage-emit-gate.d.ts +4 -4
- package/dist/runtime/stage-emit-gate.js +3 -3
- package/dist/runtime/stage-emit-gate.js.map +1 -1
- package/dist/services/AccessibilityCatalogResolver.d.ts +23 -0
- package/dist/services/AccessibilityCatalogResolver.js +200 -28
- package/dist/services/AccessibilityCatalogResolver.js.map +1 -1
- package/dist/services/HighlightCoordinator.d.ts +25 -0
- package/dist/services/HighlightCoordinator.js +125 -0
- package/dist/services/HighlightCoordinator.js.map +1 -1
- package/dist/services/SSMLExtractor.d.ts +16 -1
- package/dist/services/SSMLExtractor.js +86 -7
- package/dist/services/SSMLExtractor.js.map +1 -1
- package/dist/services/TTSService.d.ts +30 -4
- package/dist/services/TTSService.js +652 -159
- package/dist/services/TTSService.js.map +1 -1
- package/dist/services/ToolRegistry.d.ts +2 -2
- package/dist/services/ToolRegistry.js +1 -1
- package/dist/services/ToolRegistry.js.map +1 -1
- package/dist/services/ToolkitCoordinator.d.ts +16 -73
- package/dist/services/ToolkitCoordinator.js +34 -113
- package/dist/services/ToolkitCoordinator.js.map +1 -1
- package/dist/services/interfaces.d.ts +20 -2
- package/dist/services/interfaces.js.map +1 -1
- package/dist/services/tool-config-validation.d.ts +1 -1
- package/dist/services/tool-config-validation.js +7 -7
- package/dist/services/tool-config-validation.js.map +1 -1
- package/dist/services/tool-instance-id.js +1 -1
- package/dist/services/tool-instance-id.js.map +1 -1
- package/dist/services/tool-providers/TTSToolProvider.js +3 -3
- package/dist/services/tool-providers/TTSToolProvider.js.map +1 -1
- package/dist/services/tools-config-normalizer.d.ts +13 -2
- package/dist/services/tools-config-normalizer.js +3 -8
- package/dist/services/tools-config-normalizer.js.map +1 -1
- package/dist/services/tts/browser-provider.js +2 -0
- package/dist/services/tts/browser-provider.js.map +1 -1
- package/dist/services/tts/catalog-span-alignment.d.ts +39 -0
- package/dist/services/tts/catalog-span-alignment.js +300 -0
- package/dist/services/tts/catalog-span-alignment.js.map +1 -0
- package/dist/services/tts/generated-speech/assemble-plan.d.ts +33 -0
- package/dist/services/tts/generated-speech/assemble-plan.js +111 -0
- package/dist/services/tts/generated-speech/assemble-plan.js.map +1 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +25 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js +66 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js.map +1 -0
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +19 -0
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.js +71 -0
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.js.map +1 -0
- package/dist/services/tts/generated-speech/dom/types.d.ts +37 -0
- package/dist/services/tts/generated-speech/dom/types.js +2 -0
- package/dist/services/tts/generated-speech/dom/types.js.map +1 -0
- package/dist/services/tts/generated-speech/index.d.ts +17 -0
- package/dist/services/tts/generated-speech/index.js +17 -0
- package/dist/services/tts/generated-speech/index.js.map +1 -0
- package/dist/services/tts/generated-speech/math-speech-cache.d.ts +15 -0
- package/dist/services/tts/generated-speech/math-speech-cache.js +78 -0
- package/dist/services/tts/generated-speech/math-speech-cache.js.map +1 -0
- package/dist/services/tts/generated-speech/types.d.ts +104 -0
- package/dist/services/tts/generated-speech/types.js +18 -0
- package/dist/services/tts/generated-speech/types.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/boundary-normalizer.d.ts +2 -0
- package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +106 -0
- package/dist/services/tts/highlight-pipeline/boundary-normalizer.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.d.ts +6 -0
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +217 -0
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/highlight-policy.d.ts +2 -0
- package/dist/services/tts/highlight-pipeline/highlight-policy.js +32 -0
- package/dist/services/tts/highlight-pipeline/highlight-policy.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/index.d.ts +9 -0
- package/dist/services/tts/highlight-pipeline/index.js +9 -0
- package/dist/services/tts/highlight-pipeline/index.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/math-semantic-resolver.d.ts +3 -0
- package/dist/services/tts/highlight-pipeline/math-semantic-resolver.js +32 -0
- package/dist/services/tts/highlight-pipeline/math-semantic-resolver.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.d.ts +29 -0
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +44 -0
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.d.ts +2 -0
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +20 -0
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/readable-region.d.ts +1 -0
- package/dist/services/tts/highlight-pipeline/readable-region.js +19 -0
- package/dist/services/tts/highlight-pipeline/readable-region.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.d.ts +6 -0
- package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.js +198 -0
- package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/types.d.ts +72 -0
- package/dist/services/tts/highlight-pipeline/types.js +2 -0
- package/dist/services/tts/highlight-pipeline/types.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +2 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js +22 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js.map +1 -0
- package/dist/services/tts/math-alignment/index.d.ts +30 -0
- package/dist/services/tts/math-alignment/index.js +66 -0
- package/dist/services/tts/math-alignment/index.js.map +1 -0
- package/dist/services/tts/math-alignment/mathml-tokenizer.d.ts +2 -0
- package/dist/services/tts/math-alignment/mathml-tokenizer.js +317 -0
- package/dist/services/tts/math-alignment/mathml-tokenizer.js.map +1 -0
- package/dist/services/tts/math-alignment/range-resolver.d.ts +10 -0
- package/dist/services/tts/math-alignment/range-resolver.js +13 -0
- package/dist/services/tts/math-alignment/range-resolver.js.map +1 -0
- package/dist/services/tts/math-alignment/sequence-aligner.d.ts +7 -0
- package/dist/services/tts/math-alignment/sequence-aligner.js +119 -0
- package/dist/services/tts/math-alignment/sequence-aligner.js.map +1 -0
- package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +31 -0
- package/dist/services/tts/math-alignment/speech-tokenizer.js +143 -0
- package/dist/services/tts/math-alignment/speech-tokenizer.js.map +1 -0
- package/dist/services/tts/math-alignment/types.d.ts +58 -0
- package/dist/services/tts/math-alignment/types.js +2 -0
- package/dist/services/tts/math-alignment/types.js.map +1 -0
- package/dist/services/tts/math-aware-text-processing.d.ts +18 -0
- package/dist/services/tts/math-aware-text-processing.js +348 -0
- package/dist/services/tts/math-aware-text-processing.js.map +1 -0
- package/dist/services/tts/math-speech.d.ts +44 -0
- package/dist/services/tts/math-speech.js +147 -0
- package/dist/services/tts/math-speech.js.map +1 -0
- package/dist/services/tts/mathml-sanitization.d.ts +1 -0
- package/dist/services/tts/mathml-sanitization.js +170 -0
- package/dist/services/tts/mathml-sanitization.js.map +1 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +33 -0
- package/dist/services/tts/ssml/spoken-text.js +159 -0
- package/dist/services/tts/ssml/spoken-text.js.map +1 -0
- package/dist/services/tts/text-processing.d.ts +3 -0
- package/dist/services/tts/text-processing.js +30 -4
- package/dist/services/tts/text-processing.js.map +1 -1
- package/dist/services/tts-runtime-config.d.ts +17 -1
- package/dist/services/tts-runtime-config.js +11 -1
- package/dist/services/tts-runtime-config.js.map +1 -1
- package/dist/tools/registrations/accessibility-tools.js +0 -1
- package/dist/tools/registrations/accessibility-tools.js.map +1 -1
- package/dist/tools/registrations/calculator.js +41 -2
- package/dist/tools/registrations/calculator.js.map +1 -1
- package/dist/tools/tool-tag-map.js +0 -1
- package/dist/tools/tool-tag-map.js.map +1 -1
- package/package.json +8 -11
- package/dist/services/PNPToolResolver.d.ts +0 -280
- package/dist/services/PNPToolResolver.js +0 -514
- package/dist/services/PNPToolResolver.js.map +0 -1
- package/dist/services/pnp-provenance.d.ts +0 -166
- package/dist/services/pnp-provenance.js +0 -231
- 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":"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","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,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,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;
|