@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-provider.js","sourceRoot":"","sources":["../../../src/services/tts/browser-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Browser TTS Provider\n *\n * Text-to-speech provider using the browser's Web Speech API.\n * Works in all modern browsers that support SpeechSynthesis.\n *\n * Part of PIE Assessment Toolkit.\n */\n\nimport type {\n\tITTSProvider,\n\tITTSProviderImplementation,\n\tTTSConfig,\n\tTTSFeature,\n\tTTSProviderCapabilities,\n} from \"@pie-players/pie-tts\";\nimport {\n\tsegmentSentences as segmentTextToSentences,\n} from \"./text-segmentation.js\";\n\ninterface TTSSpeechSegment {\n\ttext: string;\n\tstartOffset: number;\n\tpauseMsAfter?: number;\n}\n\n/**\n * Browser TTS Provider\n *\n * Stateless factory for creating browser-based TTS implementations.\n */\nexport class BrowserTTSProvider implements ITTSProvider {\n\treadonly providerId = \"browser\";\n\treadonly providerName = \"Browser Speech Synthesis\";\n\treadonly version = \"1.0.0\";\n\n\tasync initialize(config: TTSConfig): Promise<ITTSProviderImplementation> {\n\t\t// SSR guard\n\t\tif (typeof window === \"undefined\") {\n\t\t\tthrow new Error(\"BrowserTTSProvider requires browser environment\");\n\t\t}\n\n\t\t// Check browser support\n\t\tif (!(\"speechSynthesis\" in window)) {\n\t\t\tthrow new Error(\"Browser does not support Speech Synthesis API\");\n\t\t}\n\n\t\treturn new BrowserTTSProviderImpl(config);\n\t}\n\n\tsupportsFeature(feature: TTSFeature): boolean {\n\t\t// Browser supports all TTS features\n\t\treturn true;\n\t}\n\n\tgetCapabilities(): TTSProviderCapabilities {\n\t\treturn {\n\t\t\tsupportsPause: true,\n\t\t\tsupportsResume: true,\n\t\t\tsupportsWordBoundary: true,\n\t\t\tsupportsVoiceSelection: true,\n\t\t\tsupportsRateControl: true,\n\t\t\tsupportsPitchControl: true,\n\t\t};\n\t}\n\n\tdestroy(): void {\n\t\t// No cleanup needed for browser provider\n\t}\n}\n\n/**\n * Browser TTS Provider Implementation\n *\n * Handles actual speech synthesis using the Web Speech API.\n */\nclass BrowserTTSProviderImpl implements ITTSProviderImplementation {\n\tprivate utterance: SpeechSynthesisUtterance | null = null;\n\tprivate config: TTSConfig | null = null;\n\tprivate _isPlaying = false;\n\tprivate _isPaused = false;\n\tprivate speakRunId = 0;\n\n\tconstructor(config: TTSConfig) {\n\t\tthis.config = config;\n\t}\n\n\tasync speak(text: string): Promise<void> {\n\t\tif (!this.config) {\n\t\t\tthrow new Error(\"TTS not initialized\");\n\t\t}\n\t\t// Invalidate any in-flight run and cancel current utterance.\n\t\tthis.stop();\n\t\tconst runId = this.speakRunId;\n\n\t\tconst chunks = this.splitIntoChunks(text);\n\t\tfor (const chunk of chunks) {\n\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst shouldContinue = await this.speakChunk(chunk.text, chunk.offset, runId);\n\t\t\tif (!shouldContinue) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync speakSegments(segments: TTSSpeechSegment[]): Promise<void> {\n\t\tif (!this.config) {\n\t\t\tthrow new Error(\"TTS not initialized\");\n\t\t}\n\t\tthis.stop();\n\t\tconst runId = this.speakRunId;\n\t\tfor (const segment of segments) {\n\t\t\tif (runId !== this.speakRunId) break;\n\t\t\tconst chunks = this.splitIntoChunks(segment.text);\n\t\t\tfor (const chunk of chunks) {\n\t\t\t\tif (runId !== this.speakRunId) break;\n\t\t\t\tconst shouldContinue = await this.speakChunk(\n\t\t\t\t\tchunk.text,\n\t\t\t\t\tsegment.startOffset + chunk.offset,\n\t\t\t\t\trunId,\n\t\t\t\t);\n\t\t\t\tif (!shouldContinue) break;\n\t\t\t}\n\t\t\tconst pauseMsAfter = Math.max(0, Number(segment.pauseMsAfter || 0));\n\t\t\tif (pauseMsAfter > 0 && runId === this.speakRunId) {\n\t\t\t\tconst shouldContinue = await this.waitForPause(pauseMsAfter, runId);\n\t\t\t\tif (!shouldContinue) break;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async waitForPause(pauseMs: number, runId: number): Promise<boolean> {\n\t\tawait new Promise((resolve) => setTimeout(resolve, pauseMs));\n\t\treturn runId === this.speakRunId;\n\t}\n\n\tprivate splitIntoChunks(text: string): Array<{ text: string; offset: number }> {\n\t\tconst MAX_CHUNK_LENGTH = 260;\n\t\tif (text.length <= MAX_CHUNK_LENGTH) {\n\t\t\treturn [{ text, offset: 0 }];\n\t\t}\n\n\t\tconst sentences = this.segmentSentences(text);\n\t\tconst chunks: Array<{ text: string; offset: number }> = [];\n\t\tlet currentText = \"\";\n\t\tlet currentOffset = 0;\n\n\t\tfor (const sentence of sentences) {\n\t\t\tconst trimmed = sentence.text.trim();\n\t\t\tif (!trimmed) continue;\n\t\t\tconst firstNonWhitespace = sentence.text.search(/\\S/);\n\t\t\tconst trimmedStart =\n\t\t\t\tsentence.offset + (firstNonWhitespace === -1 ? 0 : firstNonWhitespace);\n\n\t\t\tif (!currentText) {\n\t\t\t\tcurrentText = trimmed;\n\t\t\t\tcurrentOffset = trimmedStart;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst candidate = `${currentText} ${trimmed}`;\n\t\t\tif (candidate.length <= MAX_CHUNK_LENGTH) {\n\t\t\t\tcurrentText = candidate;\n\t\t\t} else {\n\t\t\t\tchunks.push({ text: currentText, offset: currentOffset });\n\t\t\t\tcurrentText = trimmed;\n\t\t\t\tcurrentOffset = trimmedStart;\n\t\t\t}\n\t\t}\n\n\t\tif (currentText) {\n\t\t\tchunks.push({ text: currentText, offset: currentOffset });\n\t\t}\n\t\treturn chunks.length ? chunks : [{ text, offset: 0 }];\n\t}\n\n\tprivate getHighlightMode(): \"word\" | \"sentence\" {\n\t\tconst providerOptions = (this.config?.providerOptions || {}) as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\treturn providerOptions.highlightMode === \"word\" ? \"word\" : \"sentence\";\n\t}\n\n\tprivate getSegmentationPolicy(): {\n\t\tuseSentenceSegmenter: boolean;\n\t\tuseWordSegmenter: boolean;\n\t\tlocale?: string;\n\t} {\n\t\tconst providerOptions = (this.config?.providerOptions || {}) as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\tconst segmenter = (providerOptions.segmenter || {}) as Record<string, unknown>;\n\t\tconst mode = segmenter.mode;\n\t\tconst useSegmenter = mode !== \"regexOnly\";\n\t\tconst locale =\n\t\t\ttypeof segmenter.locale === \"string\" && segmenter.locale.trim().length > 0\n\t\t\t\t? segmenter.locale\n\t\t\t\t: typeof providerOptions.locale === \"string\" &&\n\t\t\t\t\t providerOptions.locale.trim().length > 0\n\t\t\t\t\t? providerOptions.locale\n\t\t\t\t\t: undefined;\n\t\treturn {\n\t\t\tuseSentenceSegmenter: useSegmenter,\n\t\t\tuseWordSegmenter: useSegmenter,\n\t\t\tlocale,\n\t\t};\n\t}\n\n\tprivate segmentSentences(text: string): Array<{ text: string; offset: number }> {\n\t\tconst policy = this.getSegmentationPolicy();\n\t\treturn segmentTextToSentences(text, {\n\t\t\tlocale: policy.locale,\n\t\t\tuseSentenceSegmenter: policy.useSentenceSegmenter,\n\t\t});\n\t}\n\n\tprivate inferWordLength(text: string, index: number): number {\n\t\tconst safeIndex = Math.max(0, Math.min(index, Math.max(0, text.length - 1)));\n\t\tconst slice = text.slice(safeIndex);\n\t\tconst match = slice.match(/^\\s*([^\\s]+)/);\n\t\treturn match?.[1]?.length || 1;\n\t}\n\n\tprivate async speakChunk(\n\t\tchunkText: string,\n\t\tchunkOffset: number,\n\t\trunId: number,\n\t): Promise<boolean> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\tresolve(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.utterance = new SpeechSynthesisUtterance(chunkText);\n\n\t\t\t// Apply config\n\t\t\tif (this.config?.voice) {\n\t\t\t\tconst voices = speechSynthesis.getVoices();\n\t\t\t\tconst voice = voices.find((v) => v.name === this.config!.voice);\n\t\t\t\tif (voice) this.utterance!.voice = voice;\n\t\t\t}\n\n\t\t\tif (this.config?.rate) this.utterance.rate = this.config.rate;\n\t\t\tif (this.config?.pitch) this.utterance.pitch = this.config.pitch;\n\n\t\t\tthis.utterance.onstart = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPlaying = true;\n\t\t\t\tthis._isPaused = false;\n\t\t\t};\n\n\t\t\tthis.utterance.onend = () => {\n\t\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis._isPlaying = false;\n\t\t\t\tthis._isPaused = false;\n\t\t\t\tresolve(true);\n\t\t\t};\n\n\t\t\tthis.utterance.onerror = (event) => {\n\t\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis._isPlaying = false;\n\t\t\t\tthis._isPaused = false;\n\t\t\t\tif (event.error === \"interrupted\" || event.error === \"canceled\") {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treject(new Error(`Speech synthesis error: ${event.error}`));\n\t\t\t};\n\n\t\t\tthis.utterance.onpause = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPaused = true;\n\t\t\t};\n\n\t\t\tthis.utterance.onresume = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPaused = false;\n\t\t\t};\n\n\t\t\tthis.utterance.onboundary = (event) => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tconsole.log(\n\t\t\t\t\t\"[BrowserProvider] Boundary event:\",\n\t\t\t\t\tevent.name,\n\t\t\t\t\t\"charIndex:\",\n\t\t\t\t\tevent.charIndex,\n\t\t\t\t\t\"charLength:\",\n\t\t\t\t\tevent.charLength,\n\t\t\t\t);\n\t\t\t\tif (event.name !== \"word\" || !this.onWordBoundary) return;\n\t\t\t\tif (this.getHighlightMode() === \"sentence\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst charIndex = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tMath.min(event.charIndex, Math.max(0, chunkText.length - 1)),\n\t\t\t\t);\n\t\t\t\tconst reportedLength = Number(event.charLength || 0);\n\t\t\t\tconst inferredLength = this.inferWordLength(chunkText, charIndex);\n\t\t\t\tconst wordLength =\n\t\t\t\t\tNumber.isFinite(reportedLength) &&\n\t\t\t\t\treportedLength > 0 &&\n\t\t\t\t\treportedLength <= 80 &&\n\t\t\t\t\tcharIndex + reportedLength <= chunkText.length\n\t\t\t\t\t\t? reportedLength\n\t\t\t\t\t\t: inferredLength;\n\t\t\t\tconst word = chunkText\n\t\t\t\t\t.substring(charIndex, Math.min(chunkText.length, charIndex + wordLength))\n\t\t\t\t\t.trim();\n\t\t\t\tconst absoluteBoundaryStart = chunkOffset + charIndex;\n\t\t\t\tconsole.log(\n\t\t\t\t\t\"[BrowserProvider] Calling onWordBoundary with word:\",\n\t\t\t\t\tword,\n\t\t\t\t\t\"at position:\",\n\t\t\t\t\tabsoluteBoundaryStart,\n\t\t\t\t);\n\t\t\t\tthis.onWordBoundary(word, absoluteBoundaryStart, wordLength);\n\t\t\t};\n\n\t\t\tspeechSynthesis.speak(this.utterance);\n\t\t});\n\t}\n\n\tpause(): void {\n\t\tif (this._isPlaying && !this._isPaused) {\n\t\t\tspeechSynthesis.pause();\n\t\t}\n\t}\n\n\tresume(): void {\n\t\tif (this._isPlaying && this._isPaused) {\n\t\t\tspeechSynthesis.resume();\n\t\t}\n\t}\n\n\tstop(): void {\n\t\tthis.speakRunId += 1;\n\t\tif (this._isPlaying) {\n\t\t\tspeechSynthesis.cancel();\n\t\t}\n\t\tthis._isPlaying = false;\n\t\tthis._isPaused = false;\n\t}\n\n\tisPlaying(): boolean {\n\t\treturn this._isPlaying && !this._isPaused;\n\t}\n\n\tisPaused(): boolean {\n\t\treturn this._isPaused;\n\t}\n\n\t/**\n\t * Update settings dynamically (rate, pitch, voice)\n\t * Changes take effect on the next speak() call\n\t */\n\tupdateSettings(settings: Partial<TTSConfig>): void {\n\t\tif (!this.config) {\n\t\t\tthis.config = {} as TTSConfig;\n\t\t}\n\n\t\t// Update config with new settings\n\t\tif (settings.rate !== undefined) {\n\t\t\tthis.config.rate = settings.rate;\n\t\t}\n\t\tif (settings.pitch !== undefined) {\n\t\t\tthis.config.pitch = settings.pitch;\n\t\t}\n\t\tif (settings.voice !== undefined) {\n\t\t\tthis.config.voice = settings.voice;\n\t\t}\n\t\tif (settings.providerOptions !== undefined) {\n\t\t\tthis.config.providerOptions = {\n\t\t\t\t...(this.config.providerOptions || {}),\n\t\t\t\t...(settings.providerOptions || {}),\n\t\t\t};\n\t\t}\n\t}\n\n\tonWordBoundary?: (word: string, position: number, length?: number) => void;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"browser-provider.js","sourceRoot":"","sources":["../../../src/services/tts/browser-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Browser TTS Provider\n *\n * Text-to-speech provider using the browser's Web Speech API.\n * Works in all modern browsers that support SpeechSynthesis.\n *\n * Part of PIE Assessment Toolkit.\n */\n\nimport type {\n\tITTSProvider,\n\tITTSProviderImplementation,\n\tTTSConfig,\n\tTTSFeature,\n\tTTSProviderCapabilities,\n} from \"@pie-players/pie-tts\";\nimport {\n\tsegmentSentences as segmentTextToSentences,\n} from \"./text-segmentation.js\";\n\ninterface TTSSpeechSegment {\n\ttext: string;\n\tstartOffset: number;\n\tpauseMsAfter?: number;\n}\n\n/**\n * Browser TTS Provider\n *\n * Stateless factory for creating browser-based TTS implementations.\n */\nexport class BrowserTTSProvider implements ITTSProvider {\n\treadonly providerId = \"browser\";\n\treadonly providerName = \"Browser Speech Synthesis\";\n\treadonly version = \"1.0.0\";\n\n\tasync initialize(config: TTSConfig): Promise<ITTSProviderImplementation> {\n\t\t// SSR guard\n\t\tif (typeof window === \"undefined\") {\n\t\t\tthrow new Error(\"BrowserTTSProvider requires browser environment\");\n\t\t}\n\n\t\t// Check browser support\n\t\tif (!(\"speechSynthesis\" in window)) {\n\t\t\tthrow new Error(\"Browser does not support Speech Synthesis API\");\n\t\t}\n\n\t\treturn new BrowserTTSProviderImpl(config);\n\t}\n\n\tsupportsFeature(feature: TTSFeature): boolean {\n\t\t// Browser supports all TTS features\n\t\treturn true;\n\t}\n\n\tgetCapabilities(): TTSProviderCapabilities {\n\t\treturn {\n\t\t\tsupportsPause: true,\n\t\t\tsupportsResume: true,\n\t\t\tsupportsWordBoundary: true,\n\t\t\tsupportsVoiceSelection: true,\n\t\t\tsupportsRateControl: true,\n\t\t\tsupportsPitchControl: true,\n\t\t\t// The Web Speech API voices plain text only; SSML tags are read aloud.\n\t\t\tsupportsSSML: false,\n\t\t};\n\t}\n\n\tdestroy(): void {\n\t\t// No cleanup needed for browser provider\n\t}\n}\n\n/**\n * Browser TTS Provider Implementation\n *\n * Handles actual speech synthesis using the Web Speech API.\n */\nclass BrowserTTSProviderImpl implements ITTSProviderImplementation {\n\tprivate utterance: SpeechSynthesisUtterance | null = null;\n\tprivate config: TTSConfig | null = null;\n\tprivate _isPlaying = false;\n\tprivate _isPaused = false;\n\tprivate speakRunId = 0;\n\n\tconstructor(config: TTSConfig) {\n\t\tthis.config = config;\n\t}\n\n\tasync speak(text: string): Promise<void> {\n\t\tif (!this.config) {\n\t\t\tthrow new Error(\"TTS not initialized\");\n\t\t}\n\t\t// Invalidate any in-flight run and cancel current utterance.\n\t\tthis.stop();\n\t\tconst runId = this.speakRunId;\n\n\t\tconst chunks = this.splitIntoChunks(text);\n\t\tfor (const chunk of chunks) {\n\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst shouldContinue = await this.speakChunk(chunk.text, chunk.offset, runId);\n\t\t\tif (!shouldContinue) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync speakSegments(segments: TTSSpeechSegment[]): Promise<void> {\n\t\tif (!this.config) {\n\t\t\tthrow new Error(\"TTS not initialized\");\n\t\t}\n\t\tthis.stop();\n\t\tconst runId = this.speakRunId;\n\t\tfor (const segment of segments) {\n\t\t\tif (runId !== this.speakRunId) break;\n\t\t\tconst chunks = this.splitIntoChunks(segment.text);\n\t\t\tfor (const chunk of chunks) {\n\t\t\t\tif (runId !== this.speakRunId) break;\n\t\t\t\tconst shouldContinue = await this.speakChunk(\n\t\t\t\t\tchunk.text,\n\t\t\t\t\tsegment.startOffset + chunk.offset,\n\t\t\t\t\trunId,\n\t\t\t\t);\n\t\t\t\tif (!shouldContinue) break;\n\t\t\t}\n\t\t\tconst pauseMsAfter = Math.max(0, Number(segment.pauseMsAfter || 0));\n\t\t\tif (pauseMsAfter > 0 && runId === this.speakRunId) {\n\t\t\t\tconst shouldContinue = await this.waitForPause(pauseMsAfter, runId);\n\t\t\t\tif (!shouldContinue) break;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async waitForPause(pauseMs: number, runId: number): Promise<boolean> {\n\t\tawait new Promise((resolve) => setTimeout(resolve, pauseMs));\n\t\treturn runId === this.speakRunId;\n\t}\n\n\tprivate splitIntoChunks(text: string): Array<{ text: string; offset: number }> {\n\t\tconst MAX_CHUNK_LENGTH = 260;\n\t\tif (text.length <= MAX_CHUNK_LENGTH) {\n\t\t\treturn [{ text, offset: 0 }];\n\t\t}\n\n\t\tconst sentences = this.segmentSentences(text);\n\t\tconst chunks: Array<{ text: string; offset: number }> = [];\n\t\tlet currentText = \"\";\n\t\tlet currentOffset = 0;\n\n\t\tfor (const sentence of sentences) {\n\t\t\tconst trimmed = sentence.text.trim();\n\t\t\tif (!trimmed) continue;\n\t\t\tconst firstNonWhitespace = sentence.text.search(/\\S/);\n\t\t\tconst trimmedStart =\n\t\t\t\tsentence.offset + (firstNonWhitespace === -1 ? 0 : firstNonWhitespace);\n\n\t\t\tif (!currentText) {\n\t\t\t\tcurrentText = trimmed;\n\t\t\t\tcurrentOffset = trimmedStart;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst candidate = `${currentText} ${trimmed}`;\n\t\t\tif (candidate.length <= MAX_CHUNK_LENGTH) {\n\t\t\t\tcurrentText = candidate;\n\t\t\t} else {\n\t\t\t\tchunks.push({ text: currentText, offset: currentOffset });\n\t\t\t\tcurrentText = trimmed;\n\t\t\t\tcurrentOffset = trimmedStart;\n\t\t\t}\n\t\t}\n\n\t\tif (currentText) {\n\t\t\tchunks.push({ text: currentText, offset: currentOffset });\n\t\t}\n\t\treturn chunks.length ? chunks : [{ text, offset: 0 }];\n\t}\n\n\tprivate getHighlightMode(): \"word\" | \"sentence\" {\n\t\tconst providerOptions = (this.config?.providerOptions || {}) as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\treturn providerOptions.highlightMode === \"word\" ? \"word\" : \"sentence\";\n\t}\n\n\tprivate getSegmentationPolicy(): {\n\t\tuseSentenceSegmenter: boolean;\n\t\tuseWordSegmenter: boolean;\n\t\tlocale?: string;\n\t} {\n\t\tconst providerOptions = (this.config?.providerOptions || {}) as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\tconst segmenter = (providerOptions.segmenter || {}) as Record<string, unknown>;\n\t\tconst mode = segmenter.mode;\n\t\tconst useSegmenter = mode !== \"regexOnly\";\n\t\tconst locale =\n\t\t\ttypeof segmenter.locale === \"string\" && segmenter.locale.trim().length > 0\n\t\t\t\t? segmenter.locale\n\t\t\t\t: typeof providerOptions.locale === \"string\" &&\n\t\t\t\t\t providerOptions.locale.trim().length > 0\n\t\t\t\t\t? providerOptions.locale\n\t\t\t\t\t: undefined;\n\t\treturn {\n\t\t\tuseSentenceSegmenter: useSegmenter,\n\t\t\tuseWordSegmenter: useSegmenter,\n\t\t\tlocale,\n\t\t};\n\t}\n\n\tprivate segmentSentences(text: string): Array<{ text: string; offset: number }> {\n\t\tconst policy = this.getSegmentationPolicy();\n\t\treturn segmentTextToSentences(text, {\n\t\t\tlocale: policy.locale,\n\t\t\tuseSentenceSegmenter: policy.useSentenceSegmenter,\n\t\t});\n\t}\n\n\tprivate inferWordLength(text: string, index: number): number {\n\t\tconst safeIndex = Math.max(0, Math.min(index, Math.max(0, text.length - 1)));\n\t\tconst slice = text.slice(safeIndex);\n\t\tconst match = slice.match(/^\\s*([^\\s]+)/);\n\t\treturn match?.[1]?.length || 1;\n\t}\n\n\tprivate async speakChunk(\n\t\tchunkText: string,\n\t\tchunkOffset: number,\n\t\trunId: number,\n\t): Promise<boolean> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\tresolve(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.utterance = new SpeechSynthesisUtterance(chunkText);\n\n\t\t\t// Apply config\n\t\t\tif (this.config?.voice) {\n\t\t\t\tconst voices = speechSynthesis.getVoices();\n\t\t\t\tconst voice = voices.find((v) => v.name === this.config!.voice);\n\t\t\t\tif (voice) this.utterance!.voice = voice;\n\t\t\t}\n\n\t\t\tif (this.config?.rate) this.utterance.rate = this.config.rate;\n\t\t\tif (this.config?.pitch) this.utterance.pitch = this.config.pitch;\n\n\t\t\tthis.utterance.onstart = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPlaying = true;\n\t\t\t\tthis._isPaused = false;\n\t\t\t};\n\n\t\t\tthis.utterance.onend = () => {\n\t\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis._isPlaying = false;\n\t\t\t\tthis._isPaused = false;\n\t\t\t\tresolve(true);\n\t\t\t};\n\n\t\t\tthis.utterance.onerror = (event) => {\n\t\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis._isPlaying = false;\n\t\t\t\tthis._isPaused = false;\n\t\t\t\tif (event.error === \"interrupted\" || event.error === \"canceled\") {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treject(new Error(`Speech synthesis error: ${event.error}`));\n\t\t\t};\n\n\t\t\tthis.utterance.onpause = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPaused = true;\n\t\t\t};\n\n\t\t\tthis.utterance.onresume = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPaused = false;\n\t\t\t};\n\n\t\t\tthis.utterance.onboundary = (event) => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tconsole.log(\n\t\t\t\t\t\"[BrowserProvider] Boundary event:\",\n\t\t\t\t\tevent.name,\n\t\t\t\t\t\"charIndex:\",\n\t\t\t\t\tevent.charIndex,\n\t\t\t\t\t\"charLength:\",\n\t\t\t\t\tevent.charLength,\n\t\t\t\t);\n\t\t\t\tif (event.name !== \"word\" || !this.onWordBoundary) return;\n\t\t\t\tif (this.getHighlightMode() === \"sentence\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst charIndex = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tMath.min(event.charIndex, Math.max(0, chunkText.length - 1)),\n\t\t\t\t);\n\t\t\t\tconst reportedLength = Number(event.charLength || 0);\n\t\t\t\tconst inferredLength = this.inferWordLength(chunkText, charIndex);\n\t\t\t\tconst wordLength =\n\t\t\t\t\tNumber.isFinite(reportedLength) &&\n\t\t\t\t\treportedLength > 0 &&\n\t\t\t\t\treportedLength <= 80 &&\n\t\t\t\t\tcharIndex + reportedLength <= chunkText.length\n\t\t\t\t\t\t? reportedLength\n\t\t\t\t\t\t: inferredLength;\n\t\t\t\tconst word = chunkText\n\t\t\t\t\t.substring(charIndex, Math.min(chunkText.length, charIndex + wordLength))\n\t\t\t\t\t.trim();\n\t\t\t\tconst absoluteBoundaryStart = chunkOffset + charIndex;\n\t\t\t\tconsole.log(\n\t\t\t\t\t\"[BrowserProvider] Calling onWordBoundary with word:\",\n\t\t\t\t\tword,\n\t\t\t\t\t\"at position:\",\n\t\t\t\t\tabsoluteBoundaryStart,\n\t\t\t\t);\n\t\t\t\tthis.onWordBoundary(word, absoluteBoundaryStart, wordLength);\n\t\t\t};\n\n\t\t\tspeechSynthesis.speak(this.utterance);\n\t\t});\n\t}\n\n\tpause(): void {\n\t\tif (this._isPlaying && !this._isPaused) {\n\t\t\tspeechSynthesis.pause();\n\t\t}\n\t}\n\n\tresume(): void {\n\t\tif (this._isPlaying && this._isPaused) {\n\t\t\tspeechSynthesis.resume();\n\t\t}\n\t}\n\n\tstop(): void {\n\t\tthis.speakRunId += 1;\n\t\tif (this._isPlaying) {\n\t\t\tspeechSynthesis.cancel();\n\t\t}\n\t\tthis._isPlaying = false;\n\t\tthis._isPaused = false;\n\t}\n\n\tisPlaying(): boolean {\n\t\treturn this._isPlaying && !this._isPaused;\n\t}\n\n\tisPaused(): boolean {\n\t\treturn this._isPaused;\n\t}\n\n\t/**\n\t * Update settings dynamically (rate, pitch, voice)\n\t * Changes take effect on the next speak() call\n\t */\n\tupdateSettings(settings: Partial<TTSConfig>): void {\n\t\tif (!this.config) {\n\t\t\tthis.config = {} as TTSConfig;\n\t\t}\n\n\t\t// Update config with new settings\n\t\tif (settings.rate !== undefined) {\n\t\t\tthis.config.rate = settings.rate;\n\t\t}\n\t\tif (settings.pitch !== undefined) {\n\t\t\tthis.config.pitch = settings.pitch;\n\t\t}\n\t\tif (settings.voice !== undefined) {\n\t\t\tthis.config.voice = settings.voice;\n\t\t}\n\t\tif (settings.providerOptions !== undefined) {\n\t\t\tthis.config.providerOptions = {\n\t\t\t\t...(this.config.providerOptions || {}),\n\t\t\t\t...(settings.providerOptions || {}),\n\t\t\t};\n\t\t}\n\t}\n\n\tonWordBoundary?: (word: string, position: number, length?: number) => void;\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type CatalogChunkPlaybackMode = "exact-word" | "anchor-span" | "region-fallback";
|
|
2
|
+
export type BoundaryOffsetMode = "plain-spoken-text" | "raw-ssml" | "unsupported";
|
|
3
|
+
export interface CatalogTextToken {
|
|
4
|
+
text: string;
|
|
5
|
+
normalized: string;
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CatalogSpanAnchor {
|
|
10
|
+
spokenStart: number;
|
|
11
|
+
spokenEnd: number;
|
|
12
|
+
visibleStart: number;
|
|
13
|
+
visibleEnd: number;
|
|
14
|
+
score: number;
|
|
15
|
+
}
|
|
16
|
+
export interface CatalogSpanAlignment {
|
|
17
|
+
speechText: string;
|
|
18
|
+
spokenText: string;
|
|
19
|
+
visibleText: string;
|
|
20
|
+
playbackMode: CatalogChunkPlaybackMode;
|
|
21
|
+
boundaryOffsetMode: BoundaryOffsetMode;
|
|
22
|
+
anchors: CatalogSpanAnchor[];
|
|
23
|
+
confidence: number;
|
|
24
|
+
spokenTokens: CatalogTextToken[];
|
|
25
|
+
visibleTokens: CatalogTextToken[];
|
|
26
|
+
rawToSpokenOffsetMap: Map<number, number>;
|
|
27
|
+
}
|
|
28
|
+
export declare const createCatalogSpanAlignment: (args: {
|
|
29
|
+
speechText: string;
|
|
30
|
+
visibleText: string;
|
|
31
|
+
}) => CatalogSpanAlignment;
|
|
32
|
+
export declare const resolveSpokenBoundaryOffset: (alignment: CatalogSpanAlignment, position: number, length?: number, boundaryWord?: string) => {
|
|
33
|
+
start: number;
|
|
34
|
+
length: number;
|
|
35
|
+
} | null;
|
|
36
|
+
export declare const resolveVisibleSpanForBoundary: (alignment: CatalogSpanAlignment, spokenOffset: number) => {
|
|
37
|
+
start: number;
|
|
38
|
+
end: number;
|
|
39
|
+
} | null;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { extractSpokenText } from "./ssml/spoken-text.js";
|
|
2
|
+
import { createSpeechAlignmentTokenPattern, normalizeTextForSpeech, } from "./text-processing.js";
|
|
3
|
+
// Shared Unicode-aware tokenizer (see `createSpeechAlignmentTokenPattern`), so
|
|
4
|
+
// accented Latin and non-Latin words tokenize the same way here as in the math
|
|
5
|
+
// speech tokenizer. LIMITATION (i18n): the phrase table below is still English,
|
|
6
|
+
// so multi-word operator phrases ("plus or minus", "divided by") only align for
|
|
7
|
+
// English speech; other locales degrade to coarse region highlighting. A fresh
|
|
8
|
+
// pattern per call because of the global flag's mutable `lastIndex`.
|
|
9
|
+
const TOKEN_PATTERN = createSpeechAlignmentTokenPattern();
|
|
10
|
+
const NUMERIC_WORDS = new Map([
|
|
11
|
+
["zero", "0"],
|
|
12
|
+
["one", "1"],
|
|
13
|
+
["two", "2"],
|
|
14
|
+
["three", "3"],
|
|
15
|
+
["four", "4"],
|
|
16
|
+
["five", "5"],
|
|
17
|
+
["six", "6"],
|
|
18
|
+
["seven", "7"],
|
|
19
|
+
["eight", "8"],
|
|
20
|
+
["nine", "9"],
|
|
21
|
+
["ten", "10"],
|
|
22
|
+
]);
|
|
23
|
+
const SPOKEN_PHRASE_TARGETS = [
|
|
24
|
+
{ phrase: ["plus", "or", "minus"], visible: "±", score: 8 },
|
|
25
|
+
{ phrase: ["square", "root"], visible: "√", score: 8 },
|
|
26
|
+
{ phrase: ["divided", "by"], visible: "/", score: 7 },
|
|
27
|
+
{ phrase: ["all", "over"], visible: "/", score: 7 },
|
|
28
|
+
{ phrase: ["equals"], visible: "=", score: 7 },
|
|
29
|
+
{ phrase: ["equal"], visible: "=", score: 7 },
|
|
30
|
+
{ phrase: ["times"], visible: "\u2062", score: 7 },
|
|
31
|
+
{ phrase: ["multiplied", "by"], visible: "\u2062", score: 7 },
|
|
32
|
+
{ phrase: ["over"], visible: "/", score: 6 },
|
|
33
|
+
{ phrase: ["squared"], visible: "²", score: 6 },
|
|
34
|
+
{ phrase: ["squared"], visible: "2", score: 4 },
|
|
35
|
+
{ phrase: ["negative"], visible: "-", score: 5 },
|
|
36
|
+
{ phrase: ["minus"], visible: "-", score: 5 },
|
|
37
|
+
{ phrase: ["plus"], visible: "+", score: 5 },
|
|
38
|
+
];
|
|
39
|
+
const tokenize = (text) => {
|
|
40
|
+
const tokens = [];
|
|
41
|
+
for (const match of text.matchAll(TOKEN_PATTERN)) {
|
|
42
|
+
const value = match[0];
|
|
43
|
+
const start = match.index ?? 0;
|
|
44
|
+
tokens.push({
|
|
45
|
+
text: value,
|
|
46
|
+
normalized: value.toLowerCase(),
|
|
47
|
+
start,
|
|
48
|
+
end: start + value.length,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return tokens;
|
|
52
|
+
};
|
|
53
|
+
const normalizedTokenValue = (token) => NUMERIC_WORDS.get(token.normalized) || token.normalized;
|
|
54
|
+
const createCandidates = (spokenTokens, visibleTokens) => {
|
|
55
|
+
const candidates = [];
|
|
56
|
+
for (let spokenIndex = 0; spokenIndex < spokenTokens.length; spokenIndex++) {
|
|
57
|
+
for (let visibleIndex = 0; visibleIndex < visibleTokens.length; visibleIndex++) {
|
|
58
|
+
const spokenValue = normalizedTokenValue(spokenTokens[spokenIndex]);
|
|
59
|
+
const visibleValue = normalizedTokenValue(visibleTokens[visibleIndex]);
|
|
60
|
+
if (spokenValue === visibleValue) {
|
|
61
|
+
candidates.push({
|
|
62
|
+
spokenStartToken: spokenIndex,
|
|
63
|
+
spokenEndToken: spokenIndex + 1,
|
|
64
|
+
visibleToken: visibleIndex,
|
|
65
|
+
score: 10,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const phraseTarget of SPOKEN_PHRASE_TARGETS) {
|
|
70
|
+
const phraseTokens = spokenTokens.slice(spokenIndex, spokenIndex + phraseTarget.phrase.length);
|
|
71
|
+
if (phraseTokens.length !== phraseTarget.phrase.length)
|
|
72
|
+
continue;
|
|
73
|
+
if (!phraseTokens.every((token, tokenIndex) => token.normalized === phraseTarget.phrase[tokenIndex])) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (let visibleIndex = 0; visibleIndex < visibleTokens.length; visibleIndex++) {
|
|
77
|
+
if (visibleTokens[visibleIndex].normalized !== phraseTarget.visible) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
candidates.push({
|
|
81
|
+
spokenStartToken: spokenIndex,
|
|
82
|
+
spokenEndToken: spokenIndex + phraseTarget.phrase.length,
|
|
83
|
+
visibleToken: visibleIndex,
|
|
84
|
+
score: phraseTarget.score,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return candidates;
|
|
90
|
+
};
|
|
91
|
+
const computeAnchors = (spokenTokens, visibleTokens) => {
|
|
92
|
+
const candidates = createCandidates(spokenTokens, visibleTokens).sort((left, right) => spokenTokens[left.spokenStartToken].start -
|
|
93
|
+
spokenTokens[right.spokenStartToken].start ||
|
|
94
|
+
visibleTokens[left.visibleToken].start - visibleTokens[right.visibleToken].start);
|
|
95
|
+
const bestScore = new Array(candidates.length).fill(0);
|
|
96
|
+
const previous = new Array(candidates.length).fill(-1);
|
|
97
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
98
|
+
const candidate = candidates[i];
|
|
99
|
+
bestScore[i] = candidate.score;
|
|
100
|
+
for (let j = 0; j < i; j++) {
|
|
101
|
+
const prior = candidates[j];
|
|
102
|
+
if (prior.spokenEndToken <= candidate.spokenStartToken &&
|
|
103
|
+
prior.visibleToken < candidate.visibleToken) {
|
|
104
|
+
const spokenGap = candidate.spokenStartToken - prior.spokenEndToken;
|
|
105
|
+
const visibleGap = candidate.visibleToken - prior.visibleToken - 1;
|
|
106
|
+
const gapPenalty = Math.max(0, Math.abs(spokenGap - visibleGap) - 2);
|
|
107
|
+
const score = bestScore[j] + candidate.score - gapPenalty;
|
|
108
|
+
if (score > bestScore[i]) {
|
|
109
|
+
bestScore[i] = score;
|
|
110
|
+
previous[i] = j;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let bestIndex = -1;
|
|
116
|
+
for (let i = 0; i < bestScore.length; i++) {
|
|
117
|
+
if (bestIndex < 0 || bestScore[i] > bestScore[bestIndex]) {
|
|
118
|
+
bestIndex = i;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (bestIndex < 0)
|
|
122
|
+
return [];
|
|
123
|
+
const chosen = [];
|
|
124
|
+
let current = bestIndex;
|
|
125
|
+
while (current >= 0) {
|
|
126
|
+
chosen.unshift(candidates[current]);
|
|
127
|
+
current = previous[current];
|
|
128
|
+
}
|
|
129
|
+
return chosen.map((candidate) => {
|
|
130
|
+
const firstSpoken = spokenTokens[candidate.spokenStartToken];
|
|
131
|
+
const lastSpoken = spokenTokens[candidate.spokenEndToken - 1];
|
|
132
|
+
const visible = visibleTokens[candidate.visibleToken];
|
|
133
|
+
return {
|
|
134
|
+
spokenStart: firstSpoken.start,
|
|
135
|
+
spokenEnd: lastSpoken.end,
|
|
136
|
+
visibleStart: visible.start,
|
|
137
|
+
visibleEnd: visible.end,
|
|
138
|
+
score: candidate.score,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
const scoreConfidence = (anchors, spokenTokens, visibleTokens) => {
|
|
143
|
+
if (anchors.length === 0)
|
|
144
|
+
return 0;
|
|
145
|
+
const tokenCoverage = anchors.length / Math.max(1, Math.min(spokenTokens.length, visibleTokens.length));
|
|
146
|
+
const averageScore = anchors.reduce((total, anchor) => total + anchor.score, 0) /
|
|
147
|
+
anchors.length /
|
|
148
|
+
10;
|
|
149
|
+
return Math.min(1, tokenCoverage * averageScore);
|
|
150
|
+
};
|
|
151
|
+
export const createCatalogSpanAlignment = (args) => {
|
|
152
|
+
const extracted = extractSpokenText(args.speechText);
|
|
153
|
+
const visibleText = normalizeTextForSpeech(args.visibleText);
|
|
154
|
+
const spokenText = extracted.spokenText;
|
|
155
|
+
const spokenTokens = tokenize(spokenText);
|
|
156
|
+
const visibleTokens = tokenize(visibleText);
|
|
157
|
+
const anchors = extracted.unsupportedSemantic
|
|
158
|
+
? []
|
|
159
|
+
: computeAnchors(spokenTokens, visibleTokens);
|
|
160
|
+
const confidence = scoreConfidence(anchors, spokenTokens, visibleTokens);
|
|
161
|
+
const isExact = spokenText === visibleText && spokenText.length > 0;
|
|
162
|
+
const hasShortSingleExactAnchor = anchors.length === 1 &&
|
|
163
|
+
anchors[0].score === 10 &&
|
|
164
|
+
spokenTokens.length <= 2 &&
|
|
165
|
+
visibleTokens.length <= 2;
|
|
166
|
+
const playbackMode = extracted.unsupportedSemantic
|
|
167
|
+
? "region-fallback"
|
|
168
|
+
: isExact
|
|
169
|
+
? "exact-word"
|
|
170
|
+
: anchors.length >= 2 || hasShortSingleExactAnchor
|
|
171
|
+
? confidence >= 0.12
|
|
172
|
+
? "anchor-span"
|
|
173
|
+
: "region-fallback"
|
|
174
|
+
: "region-fallback";
|
|
175
|
+
const boundaryOffsetMode = extracted.unsupportedSemantic
|
|
176
|
+
? "unsupported"
|
|
177
|
+
: extracted.hasMarkup
|
|
178
|
+
? "raw-ssml"
|
|
179
|
+
: "plain-spoken-text";
|
|
180
|
+
return {
|
|
181
|
+
speechText: args.speechText,
|
|
182
|
+
spokenText,
|
|
183
|
+
visibleText,
|
|
184
|
+
playbackMode,
|
|
185
|
+
boundaryOffsetMode,
|
|
186
|
+
anchors,
|
|
187
|
+
confidence,
|
|
188
|
+
spokenTokens,
|
|
189
|
+
visibleTokens,
|
|
190
|
+
rawToSpokenOffsetMap: extracted.rawToSpokenOffsetMap,
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
const findTokenAtOffset = (tokens, offset) => tokens.find((token) => token.start <= offset && offset < token.end) || null;
|
|
194
|
+
const normalizeBoundaryWord = (word) => {
|
|
195
|
+
if (!word)
|
|
196
|
+
return null;
|
|
197
|
+
if (/^<[^>]+>$/.test(word.trim()))
|
|
198
|
+
return null;
|
|
199
|
+
const token = tokenize(word)[0];
|
|
200
|
+
if (!token)
|
|
201
|
+
return null;
|
|
202
|
+
return NUMERIC_WORDS.get(token.normalized) || token.normalized;
|
|
203
|
+
};
|
|
204
|
+
const candidateMatchesBoundaryWord = (alignment, candidate, boundaryWord) => {
|
|
205
|
+
const normalizedBoundary = normalizeBoundaryWord(boundaryWord);
|
|
206
|
+
if (!candidate || !normalizedBoundary)
|
|
207
|
+
return false;
|
|
208
|
+
const token = findTokenAtOffset(alignment.spokenTokens, candidate.start);
|
|
209
|
+
if (!token)
|
|
210
|
+
return false;
|
|
211
|
+
return normalizedTokenValue(token) === normalizedBoundary;
|
|
212
|
+
};
|
|
213
|
+
const mapRawOffsetToSpokenOffset = (alignment, position) => {
|
|
214
|
+
const direct = alignment.rawToSpokenOffsetMap.get(position);
|
|
215
|
+
if (direct !== undefined)
|
|
216
|
+
return direct;
|
|
217
|
+
for (let offset = position; offset < alignment.speechText.length; offset++) {
|
|
218
|
+
const mapped = alignment.rawToSpokenOffsetMap.get(offset);
|
|
219
|
+
if (mapped !== undefined)
|
|
220
|
+
return mapped;
|
|
221
|
+
if (alignment.speechText[offset] === ">")
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
};
|
|
226
|
+
const resolveRawBoundaryOffset = (alignment, position, safeLength) => {
|
|
227
|
+
const mappedStart = mapRawOffsetToSpokenOffset(alignment, position);
|
|
228
|
+
if (mappedStart === null)
|
|
229
|
+
return null;
|
|
230
|
+
const mappedEnd = mapRawOffsetToSpokenOffset(alignment, Math.max(position, position + safeLength - 1));
|
|
231
|
+
const token = findTokenAtOffset(alignment.spokenTokens, mappedStart);
|
|
232
|
+
const mappedLength = mappedEnd !== null && mappedEnd >= mappedStart
|
|
233
|
+
? mappedEnd - mappedStart + 1
|
|
234
|
+
: token
|
|
235
|
+
? token.end - mappedStart
|
|
236
|
+
: safeLength;
|
|
237
|
+
return { start: mappedStart, length: Math.max(1, mappedLength) };
|
|
238
|
+
};
|
|
239
|
+
const resolvePlainBoundaryOffset = (alignment, position, safeLength) => {
|
|
240
|
+
if (position < 0 || position >= alignment.spokenText.length)
|
|
241
|
+
return null;
|
|
242
|
+
const token = findTokenAtOffset(alignment.spokenTokens, position);
|
|
243
|
+
if (!token)
|
|
244
|
+
return null;
|
|
245
|
+
return { start: position, length: Math.min(safeLength, token.end - position) };
|
|
246
|
+
};
|
|
247
|
+
export const resolveSpokenBoundaryOffset = (alignment, position, length = 1, boundaryWord) => {
|
|
248
|
+
if (alignment.boundaryOffsetMode === "unsupported" ||
|
|
249
|
+
!Number.isFinite(position)) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const safeLength = Math.max(1, Number.isFinite(length) ? length : 1);
|
|
253
|
+
if (boundaryWord && /^<[^>]+>$/.test(boundaryWord.trim()))
|
|
254
|
+
return null;
|
|
255
|
+
if (alignment.boundaryOffsetMode === "raw-ssml") {
|
|
256
|
+
const rawCandidate = resolveRawBoundaryOffset(alignment, position, safeLength);
|
|
257
|
+
const plainCandidate = resolvePlainBoundaryOffset(alignment, position, safeLength);
|
|
258
|
+
if (boundaryWord) {
|
|
259
|
+
if (candidateMatchesBoundaryWord(alignment, rawCandidate, boundaryWord)) {
|
|
260
|
+
return rawCandidate;
|
|
261
|
+
}
|
|
262
|
+
if (candidateMatchesBoundaryWord(alignment, plainCandidate, boundaryWord)) {
|
|
263
|
+
return plainCandidate;
|
|
264
|
+
}
|
|
265
|
+
if (normalizeBoundaryWord(boundaryWord))
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return rawCandidate || plainCandidate;
|
|
269
|
+
}
|
|
270
|
+
return (resolvePlainBoundaryOffset(alignment, position, safeLength) ||
|
|
271
|
+
resolveRawBoundaryOffset(alignment, position, safeLength));
|
|
272
|
+
};
|
|
273
|
+
export const resolveVisibleSpanForBoundary = (alignment, spokenOffset) => {
|
|
274
|
+
if (alignment.playbackMode === "region-fallback")
|
|
275
|
+
return null;
|
|
276
|
+
const exactAnchor = alignment.anchors.find((anchor) => anchor.spokenStart <= spokenOffset && spokenOffset < anchor.spokenEnd);
|
|
277
|
+
if (exactAnchor) {
|
|
278
|
+
return { start: exactAnchor.visibleStart, end: exactAnchor.visibleEnd };
|
|
279
|
+
}
|
|
280
|
+
let previous = null;
|
|
281
|
+
let next = null;
|
|
282
|
+
for (const anchor of alignment.anchors) {
|
|
283
|
+
if (anchor.spokenEnd <= spokenOffset) {
|
|
284
|
+
previous = anchor;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (anchor.spokenStart > spokenOffset) {
|
|
288
|
+
next = anchor;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (previous && next) {
|
|
293
|
+
return {
|
|
294
|
+
start: Math.min(previous.visibleStart, next.visibleStart),
|
|
295
|
+
end: Math.max(previous.visibleEnd, next.visibleEnd),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
};
|
|
300
|
+
//# sourceMappingURL=catalog-span-alignment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-span-alignment.js","sourceRoot":"","sources":["../../../src/services/tts/catalog-span-alignment.ts"],"names":[],"mappings":"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","sourcesContent":["import { extractSpokenText } from \"./ssml/spoken-text.js\";\nimport {\n\tcreateSpeechAlignmentTokenPattern,\n\tnormalizeTextForSpeech,\n} from \"./text-processing.js\";\n\nexport type CatalogChunkPlaybackMode =\n\t| \"exact-word\"\n\t| \"anchor-span\"\n\t| \"region-fallback\";\n\nexport type BoundaryOffsetMode =\n\t| \"plain-spoken-text\"\n\t| \"raw-ssml\"\n\t| \"unsupported\";\n\nexport interface CatalogTextToken {\n\ttext: string;\n\tnormalized: string;\n\tstart: number;\n\tend: number;\n}\n\nexport interface CatalogSpanAnchor {\n\tspokenStart: number;\n\tspokenEnd: number;\n\tvisibleStart: number;\n\tvisibleEnd: number;\n\tscore: number;\n}\n\nexport interface CatalogSpanAlignment {\n\tspeechText: string;\n\tspokenText: string;\n\tvisibleText: string;\n\tplaybackMode: CatalogChunkPlaybackMode;\n\tboundaryOffsetMode: BoundaryOffsetMode;\n\tanchors: CatalogSpanAnchor[];\n\tconfidence: number;\n\tspokenTokens: CatalogTextToken[];\n\tvisibleTokens: CatalogTextToken[];\n\trawToSpokenOffsetMap: Map<number, number>;\n}\n\ninterface MatchCandidate {\n\tspokenStartToken: number;\n\tspokenEndToken: number;\n\tvisibleToken: number;\n\tscore: number;\n}\n\n// Shared Unicode-aware tokenizer (see `createSpeechAlignmentTokenPattern`), so\n// accented Latin and non-Latin words tokenize the same way here as in the math\n// speech tokenizer. LIMITATION (i18n): the phrase table below is still English,\n// so multi-word operator phrases (\"plus or minus\", \"divided by\") only align for\n// English speech; other locales degrade to coarse region highlighting. A fresh\n// pattern per call because of the global flag's mutable `lastIndex`.\nconst TOKEN_PATTERN = createSpeechAlignmentTokenPattern();\n\nconst NUMERIC_WORDS = new Map<string, string>([\n\t[\"zero\", \"0\"],\n\t[\"one\", \"1\"],\n\t[\"two\", \"2\"],\n\t[\"three\", \"3\"],\n\t[\"four\", \"4\"],\n\t[\"five\", \"5\"],\n\t[\"six\", \"6\"],\n\t[\"seven\", \"7\"],\n\t[\"eight\", \"8\"],\n\t[\"nine\", \"9\"],\n\t[\"ten\", \"10\"],\n]);\n\nconst SPOKEN_PHRASE_TARGETS: Array<{\n\tphrase: string[];\n\tvisible: string;\n\tscore: number;\n}> = [\n\t{ phrase: [\"plus\", \"or\", \"minus\"], visible: \"±\", score: 8 },\n\t{ phrase: [\"square\", \"root\"], visible: \"√\", score: 8 },\n\t{ phrase: [\"divided\", \"by\"], visible: \"/\", score: 7 },\n\t{ phrase: [\"all\", \"over\"], visible: \"/\", score: 7 },\n\t{ phrase: [\"equals\"], visible: \"=\", score: 7 },\n\t{ phrase: [\"equal\"], visible: \"=\", score: 7 },\n\t{ phrase: [\"times\"], visible: \"\\u2062\", score: 7 },\n\t{ phrase: [\"multiplied\", \"by\"], visible: \"\\u2062\", score: 7 },\n\t{ phrase: [\"over\"], visible: \"/\", score: 6 },\n\t{ phrase: [\"squared\"], visible: \"²\", score: 6 },\n\t{ phrase: [\"squared\"], visible: \"2\", score: 4 },\n\t{ phrase: [\"negative\"], visible: \"-\", score: 5 },\n\t{ phrase: [\"minus\"], visible: \"-\", score: 5 },\n\t{ phrase: [\"plus\"], visible: \"+\", score: 5 },\n];\n\nconst tokenize = (text: string): CatalogTextToken[] => {\n\tconst tokens: CatalogTextToken[] = [];\n\tfor (const match of text.matchAll(TOKEN_PATTERN)) {\n\t\tconst value = match[0];\n\t\tconst start = match.index ?? 0;\n\t\ttokens.push({\n\t\t\ttext: value,\n\t\t\tnormalized: value.toLowerCase(),\n\t\t\tstart,\n\t\t\tend: start + value.length,\n\t\t});\n\t}\n\treturn tokens;\n};\n\nconst normalizedTokenValue = (token: CatalogTextToken): string =>\n\tNUMERIC_WORDS.get(token.normalized) || token.normalized;\n\nconst createCandidates = (\n\tspokenTokens: CatalogTextToken[],\n\tvisibleTokens: CatalogTextToken[],\n): MatchCandidate[] => {\n\tconst candidates: MatchCandidate[] = [];\n\tfor (let spokenIndex = 0; spokenIndex < spokenTokens.length; spokenIndex++) {\n\t\tfor (let visibleIndex = 0; visibleIndex < visibleTokens.length; visibleIndex++) {\n\t\t\tconst spokenValue = normalizedTokenValue(spokenTokens[spokenIndex]);\n\t\t\tconst visibleValue = normalizedTokenValue(visibleTokens[visibleIndex]);\n\t\t\tif (spokenValue === visibleValue) {\n\t\t\t\tcandidates.push({\n\t\t\t\t\tspokenStartToken: spokenIndex,\n\t\t\t\t\tspokenEndToken: spokenIndex + 1,\n\t\t\t\t\tvisibleToken: visibleIndex,\n\t\t\t\t\tscore: 10,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tfor (const phraseTarget of SPOKEN_PHRASE_TARGETS) {\n\t\t\tconst phraseTokens = spokenTokens.slice(\n\t\t\t\tspokenIndex,\n\t\t\t\tspokenIndex + phraseTarget.phrase.length,\n\t\t\t);\n\t\t\tif (phraseTokens.length !== phraseTarget.phrase.length) continue;\n\t\t\tif (\n\t\t\t\t!phraseTokens.every(\n\t\t\t\t\t(token, tokenIndex) =>\n\t\t\t\t\t\ttoken.normalized === phraseTarget.phrase[tokenIndex],\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (\n\t\t\t\tlet visibleIndex = 0;\n\t\t\t\tvisibleIndex < visibleTokens.length;\n\t\t\t\tvisibleIndex++\n\t\t\t) {\n\t\t\t\tif (visibleTokens[visibleIndex].normalized !== phraseTarget.visible) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tcandidates.push({\n\t\t\t\t\tspokenStartToken: spokenIndex,\n\t\t\t\t\tspokenEndToken: spokenIndex + phraseTarget.phrase.length,\n\t\t\t\t\tvisibleToken: visibleIndex,\n\t\t\t\t\tscore: phraseTarget.score,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn candidates;\n};\n\nconst computeAnchors = (\n\tspokenTokens: CatalogTextToken[],\n\tvisibleTokens: CatalogTextToken[],\n): CatalogSpanAnchor[] => {\n\tconst candidates = createCandidates(spokenTokens, visibleTokens).sort(\n\t\t(left, right) =>\n\t\t\tspokenTokens[left.spokenStartToken].start -\n\t\t\t\tspokenTokens[right.spokenStartToken].start ||\n\t\t\tvisibleTokens[left.visibleToken].start - visibleTokens[right.visibleToken].start,\n\t);\n\tconst bestScore = new Array(candidates.length).fill(0) as number[];\n\tconst previous = new Array(candidates.length).fill(-1) as number[];\n\n\tfor (let i = 0; i < candidates.length; i++) {\n\t\tconst candidate = candidates[i];\n\t\tbestScore[i] = candidate.score;\n\t\tfor (let j = 0; j < i; j++) {\n\t\t\tconst prior = candidates[j];\n\t\t\tif (\n\t\t\t\tprior.spokenEndToken <= candidate.spokenStartToken &&\n\t\t\t\tprior.visibleToken < candidate.visibleToken\n\t\t\t) {\n\t\t\t\tconst spokenGap = candidate.spokenStartToken - prior.spokenEndToken;\n\t\t\t\tconst visibleGap = candidate.visibleToken - prior.visibleToken - 1;\n\t\t\t\tconst gapPenalty = Math.max(0, Math.abs(spokenGap - visibleGap) - 2);\n\t\t\t\tconst score = bestScore[j] + candidate.score - gapPenalty;\n\t\t\t\tif (score > bestScore[i]) {\n\t\t\t\t\tbestScore[i] = score;\n\t\t\t\t\tprevious[i] = j;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tlet bestIndex = -1;\n\tfor (let i = 0; i < bestScore.length; i++) {\n\t\tif (bestIndex < 0 || bestScore[i] > bestScore[bestIndex]) {\n\t\t\tbestIndex = i;\n\t\t}\n\t}\n\tif (bestIndex < 0) return [];\n\n\tconst chosen: MatchCandidate[] = [];\n\tlet current = bestIndex;\n\twhile (current >= 0) {\n\t\tchosen.unshift(candidates[current]);\n\t\tcurrent = previous[current];\n\t}\n\n\treturn chosen.map((candidate) => {\n\t\tconst firstSpoken = spokenTokens[candidate.spokenStartToken];\n\t\tconst lastSpoken = spokenTokens[candidate.spokenEndToken - 1];\n\t\tconst visible = visibleTokens[candidate.visibleToken];\n\t\treturn {\n\t\t\tspokenStart: firstSpoken.start,\n\t\t\tspokenEnd: lastSpoken.end,\n\t\t\tvisibleStart: visible.start,\n\t\t\tvisibleEnd: visible.end,\n\t\t\tscore: candidate.score,\n\t\t};\n\t});\n};\n\nconst scoreConfidence = (\n\tanchors: CatalogSpanAnchor[],\n\tspokenTokens: CatalogTextToken[],\n\tvisibleTokens: CatalogTextToken[],\n): number => {\n\tif (anchors.length === 0) return 0;\n\tconst tokenCoverage =\n\t\tanchors.length / Math.max(1, Math.min(spokenTokens.length, visibleTokens.length));\n\tconst averageScore =\n\t\tanchors.reduce((total, anchor) => total + anchor.score, 0) /\n\t\tanchors.length /\n\t\t10;\n\treturn Math.min(1, tokenCoverage * averageScore);\n};\n\nexport const createCatalogSpanAlignment = (args: {\n\tspeechText: string;\n\tvisibleText: string;\n}): CatalogSpanAlignment => {\n\tconst extracted = extractSpokenText(args.speechText);\n\tconst visibleText = normalizeTextForSpeech(args.visibleText);\n\tconst spokenText = extracted.spokenText;\n\tconst spokenTokens = tokenize(spokenText);\n\tconst visibleTokens = tokenize(visibleText);\n\tconst anchors = extracted.unsupportedSemantic\n\t\t? []\n\t\t: computeAnchors(spokenTokens, visibleTokens);\n\tconst confidence = scoreConfidence(anchors, spokenTokens, visibleTokens);\n\tconst isExact = spokenText === visibleText && spokenText.length > 0;\n\tconst hasShortSingleExactAnchor =\n\t\tanchors.length === 1 &&\n\t\tanchors[0].score === 10 &&\n\t\tspokenTokens.length <= 2 &&\n\t\tvisibleTokens.length <= 2;\n\tconst playbackMode: CatalogChunkPlaybackMode = extracted.unsupportedSemantic\n\t\t? \"region-fallback\"\n\t\t: isExact\n\t\t\t? \"exact-word\"\n\t\t\t: anchors.length >= 2 || hasShortSingleExactAnchor\n\t\t\t\t? confidence >= 0.12\n\t\t\t\t\t? \"anchor-span\"\n\t\t\t\t\t: \"region-fallback\"\n\t\t\t\t: \"region-fallback\";\n\tconst boundaryOffsetMode: BoundaryOffsetMode = extracted.unsupportedSemantic\n\t\t? \"unsupported\"\n\t\t: extracted.hasMarkup\n\t\t\t? \"raw-ssml\"\n\t\t\t: \"plain-spoken-text\";\n\n\treturn {\n\t\tspeechText: args.speechText,\n\t\tspokenText,\n\t\tvisibleText,\n\t\tplaybackMode,\n\t\tboundaryOffsetMode,\n\t\tanchors,\n\t\tconfidence,\n\t\tspokenTokens,\n\t\tvisibleTokens,\n\t\trawToSpokenOffsetMap: extracted.rawToSpokenOffsetMap,\n\t};\n};\n\nconst findTokenAtOffset = (\n\ttokens: CatalogTextToken[],\n\toffset: number,\n): CatalogTextToken | null =>\n\ttokens.find((token) => token.start <= offset && offset < token.end) || null;\n\nconst normalizeBoundaryWord = (word?: string): string | null => {\n\tif (!word) return null;\n\tif (/^<[^>]+>$/.test(word.trim())) return null;\n\tconst token = tokenize(word)[0];\n\tif (!token) return null;\n\treturn NUMERIC_WORDS.get(token.normalized) || token.normalized;\n};\n\nconst candidateMatchesBoundaryWord = (\n\talignment: CatalogSpanAlignment,\n\tcandidate: { start: number; length: number } | null,\n\tboundaryWord?: string,\n): boolean => {\n\tconst normalizedBoundary = normalizeBoundaryWord(boundaryWord);\n\tif (!candidate || !normalizedBoundary) return false;\n\tconst token = findTokenAtOffset(alignment.spokenTokens, candidate.start);\n\tif (!token) return false;\n\treturn normalizedTokenValue(token) === normalizedBoundary;\n};\n\nconst mapRawOffsetToSpokenOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n): number | null => {\n\tconst direct = alignment.rawToSpokenOffsetMap.get(position);\n\tif (direct !== undefined) return direct;\n\tfor (let offset = position; offset < alignment.speechText.length; offset++) {\n\t\tconst mapped = alignment.rawToSpokenOffsetMap.get(offset);\n\t\tif (mapped !== undefined) return mapped;\n\t\tif (alignment.speechText[offset] === \">\") break;\n\t}\n\treturn null;\n};\n\nconst resolveRawBoundaryOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n\tsafeLength: number,\n): { start: number; length: number } | null => {\n\tconst mappedStart = mapRawOffsetToSpokenOffset(alignment, position);\n\tif (mappedStart === null) return null;\n\tconst mappedEnd = mapRawOffsetToSpokenOffset(\n\t\talignment,\n\t\tMath.max(position, position + safeLength - 1),\n\t);\n\tconst token = findTokenAtOffset(alignment.spokenTokens, mappedStart);\n\tconst mappedLength =\n\t\tmappedEnd !== null && mappedEnd >= mappedStart\n\t\t\t? mappedEnd - mappedStart + 1\n\t\t\t: token\n\t\t\t\t? token.end - mappedStart\n\t\t\t\t: safeLength;\n\treturn { start: mappedStart, length: Math.max(1, mappedLength) };\n};\n\nconst resolvePlainBoundaryOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n\tsafeLength: number,\n): { start: number; length: number } | null => {\n\tif (position < 0 || position >= alignment.spokenText.length) return null;\n\tconst token = findTokenAtOffset(alignment.spokenTokens, position);\n\tif (!token) return null;\n\treturn { start: position, length: Math.min(safeLength, token.end - position) };\n};\n\nexport const resolveSpokenBoundaryOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n\tlength = 1,\n\tboundaryWord?: string,\n): { start: number; length: number } | null => {\n\tif (\n\t\talignment.boundaryOffsetMode === \"unsupported\" ||\n\t\t!Number.isFinite(position)\n\t) {\n\t\treturn null;\n\t}\n\tconst safeLength = Math.max(1, Number.isFinite(length) ? length : 1);\n\tif (boundaryWord && /^<[^>]+>$/.test(boundaryWord.trim())) return null;\n\tif (alignment.boundaryOffsetMode === \"raw-ssml\") {\n\t\tconst rawCandidate = resolveRawBoundaryOffset(\n\t\t\talignment,\n\t\t\tposition,\n\t\t\tsafeLength,\n\t\t);\n\t\tconst plainCandidate = resolvePlainBoundaryOffset(\n\t\t\talignment,\n\t\t\tposition,\n\t\t\tsafeLength,\n\t\t);\n\t\tif (boundaryWord) {\n\t\t\tif (\n\t\t\t\tcandidateMatchesBoundaryWord(alignment, rawCandidate, boundaryWord)\n\t\t\t) {\n\t\t\t\treturn rawCandidate;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcandidateMatchesBoundaryWord(alignment, plainCandidate, boundaryWord)\n\t\t\t) {\n\t\t\t\treturn plainCandidate;\n\t\t\t}\n\t\t\tif (normalizeBoundaryWord(boundaryWord)) return null;\n\t\t}\n\t\treturn rawCandidate || plainCandidate;\n\t}\n\treturn (\n\t\tresolvePlainBoundaryOffset(alignment, position, safeLength) ||\n\t\tresolveRawBoundaryOffset(alignment, position, safeLength)\n\t);\n};\n\nexport const resolveVisibleSpanForBoundary = (\n\talignment: CatalogSpanAlignment,\n\tspokenOffset: number,\n): { start: number; end: number } | null => {\n\tif (alignment.playbackMode === \"region-fallback\") return null;\n\tconst exactAnchor = alignment.anchors.find(\n\t\t(anchor) => anchor.spokenStart <= spokenOffset && spokenOffset < anchor.spokenEnd,\n\t);\n\tif (exactAnchor) {\n\t\treturn { start: exactAnchor.visibleStart, end: exactAnchor.visibleEnd };\n\t}\n\n\tlet previous: CatalogSpanAnchor | null = null;\n\tlet next: CatalogSpanAnchor | null = null;\n\tfor (const anchor of alignment.anchors) {\n\t\tif (anchor.spokenEnd <= spokenOffset) {\n\t\t\tprevious = anchor;\n\t\t\tcontinue;\n\t\t}\n\t\tif (anchor.spokenStart > spokenOffset) {\n\t\t\tnext = anchor;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (previous && next) {\n\t\treturn {\n\t\t\tstart: Math.min(previous.visibleStart, next.visibleStart),\n\t\t\tend: Math.max(previous.visibleEnd, next.visibleEnd),\n\t\t};\n\t}\n\treturn null;\n};\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MathAwareSpeechChunk } from "../math-aware-text-processing.js";
|
|
2
|
+
import { type ResolvedMathSpeech, type SREMathSpeechOptions } from "../math-speech.js";
|
|
3
|
+
import type { AssembledSpeech } from "./types.js";
|
|
4
|
+
type MathChunk = Extract<MathAwareSpeechChunk, {
|
|
5
|
+
type: "math";
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves spoken text for a single math chunk. Injectable so the runtime can
|
|
9
|
+
* supply a memoized resolver (deterministic caching) and tests can stub SRE.
|
|
10
|
+
* Mirrors `resolveMathSpeechFromChunks([chunk], options)`.
|
|
11
|
+
*/
|
|
12
|
+
export type MathSpeechResolver = (chunk: MathChunk, options: {
|
|
13
|
+
language?: string;
|
|
14
|
+
produceSsml?: boolean;
|
|
15
|
+
mathSpeech?: SREMathSpeechOptions;
|
|
16
|
+
}) => Promise<ResolvedMathSpeech>;
|
|
17
|
+
/**
|
|
18
|
+
* Assemble an anchor-free speech plan from already-collected math-aware chunks.
|
|
19
|
+
*
|
|
20
|
+
* PURE: no DOM walking, no app-service imports. Preserves the spoken-text and
|
|
21
|
+
* visible-span bookkeeping from the inline generated path, so the runtime output
|
|
22
|
+
* is stable once serialized to plain text.
|
|
23
|
+
*/
|
|
24
|
+
export declare const assembleGeneratedSpeech: (args: {
|
|
25
|
+
chunks: ReadonlyArray<MathAwareSpeechChunk>;
|
|
26
|
+
visibleText: string;
|
|
27
|
+
language?: string;
|
|
28
|
+
mathSpeech?: SREMathSpeechOptions;
|
|
29
|
+
/** Request SRE SSML per math equation (used by the SSML playback format). */
|
|
30
|
+
produceSsml?: boolean;
|
|
31
|
+
resolveMathSpeech?: MathSpeechResolver;
|
|
32
|
+
}) => Promise<AssembledSpeech>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { resolveMathSpeechFromChunks, } from "../math-speech.js";
|
|
2
|
+
import { normalizeTextForSpeech } from "../text-processing.js";
|
|
3
|
+
import { segmentSentences as segmentTextToSentences } from "../text-segmentation.js";
|
|
4
|
+
const defaultMathSpeechResolver = (chunk, options) => resolveMathSpeechFromChunks([chunk], options);
|
|
5
|
+
const normalizeLocale = (language) => (language || "en").split("-")[0].toLowerCase() || "en";
|
|
6
|
+
const PRESERVE_PROSE_CHUNK_TAGS = new Set([
|
|
7
|
+
"H1",
|
|
8
|
+
"H2",
|
|
9
|
+
"H3",
|
|
10
|
+
"H4",
|
|
11
|
+
"H5",
|
|
12
|
+
"H6",
|
|
13
|
+
"LI",
|
|
14
|
+
"LABEL",
|
|
15
|
+
"BUTTON",
|
|
16
|
+
]);
|
|
17
|
+
const PRESERVE_PROSE_CHUNK_ROLES = new Set([
|
|
18
|
+
"heading",
|
|
19
|
+
"listitem",
|
|
20
|
+
"option",
|
|
21
|
+
"radio",
|
|
22
|
+
]);
|
|
23
|
+
const shouldPreserveProseChunk = (chunk) => {
|
|
24
|
+
if (chunk.type !== "text" || !chunk.sourceElement)
|
|
25
|
+
return false;
|
|
26
|
+
const tagName = chunk.sourceElement.tagName.toUpperCase();
|
|
27
|
+
const role = (chunk.sourceElement.getAttribute("role") || "").toLowerCase();
|
|
28
|
+
return (PRESERVE_PROSE_CHUNK_TAGS.has(tagName) ||
|
|
29
|
+
PRESERVE_PROSE_CHUNK_ROLES.has(role));
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Assemble an anchor-free speech plan from already-collected math-aware chunks.
|
|
33
|
+
*
|
|
34
|
+
* PURE: no DOM walking, no app-service imports. Preserves the spoken-text and
|
|
35
|
+
* visible-span bookkeeping from the inline generated path, so the runtime output
|
|
36
|
+
* is stable once serialized to plain text.
|
|
37
|
+
*/
|
|
38
|
+
export const assembleGeneratedSpeech = async (args) => {
|
|
39
|
+
const resolveMathSpeech = args.resolveMathSpeech ?? defaultMathSpeechResolver;
|
|
40
|
+
const { visibleText } = args;
|
|
41
|
+
const segments = [];
|
|
42
|
+
// Cursor-based span tracking:
|
|
43
|
+
// locate each chunk's normalized text within the aggregate visible text,
|
|
44
|
+
// advancing a cursor so math fallbacks reserve their span even though only
|
|
45
|
+
// prose spans are sliced for highlighting.
|
|
46
|
+
let visibleCursor = 0;
|
|
47
|
+
const nextVisibleSpan = (text) => {
|
|
48
|
+
const normalized = normalizeTextForSpeech(text);
|
|
49
|
+
const found = visibleText.indexOf(normalized, visibleCursor);
|
|
50
|
+
const start = found >= 0 ? found : visibleCursor;
|
|
51
|
+
const end = start + normalized.length;
|
|
52
|
+
visibleCursor = end;
|
|
53
|
+
return { start, end };
|
|
54
|
+
};
|
|
55
|
+
const speechParts = [];
|
|
56
|
+
for (let index = 0; index < args.chunks.length; index++) {
|
|
57
|
+
const chunk = args.chunks[index];
|
|
58
|
+
if (chunk.type === "text") {
|
|
59
|
+
const proseSegments = shouldPreserveProseChunk(chunk)
|
|
60
|
+
? [{ text: chunk.text, offset: 0 }]
|
|
61
|
+
: segmentTextToSentences(chunk.text, {
|
|
62
|
+
locale: args.language,
|
|
63
|
+
});
|
|
64
|
+
for (const prose of proseSegments) {
|
|
65
|
+
const text = normalizeTextForSpeech(prose.text);
|
|
66
|
+
if (!text)
|
|
67
|
+
continue;
|
|
68
|
+
const visibleSpan = nextVisibleSpan(text);
|
|
69
|
+
segments.push({
|
|
70
|
+
sourceChunkIndex: index,
|
|
71
|
+
segment: {
|
|
72
|
+
kind: "prose",
|
|
73
|
+
spokenText: text,
|
|
74
|
+
visibleText: text,
|
|
75
|
+
visibleSpan,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
speechParts.push(text);
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const visibleSpan = nextVisibleSpan(chunk.fallbackText);
|
|
83
|
+
const generated = await resolveMathSpeech(chunk, {
|
|
84
|
+
language: args.language,
|
|
85
|
+
produceSsml: args.produceSsml,
|
|
86
|
+
mathSpeech: args.mathSpeech,
|
|
87
|
+
});
|
|
88
|
+
const usedFallback = !generated.speechText;
|
|
89
|
+
const spokenText = generated.speechText || chunk.fallbackText;
|
|
90
|
+
segments.push({
|
|
91
|
+
sourceChunkIndex: index,
|
|
92
|
+
segment: {
|
|
93
|
+
kind: "math",
|
|
94
|
+
spokenText,
|
|
95
|
+
mathml: chunk.mathml,
|
|
96
|
+
fallbackText: chunk.fallbackText,
|
|
97
|
+
visibleSpan,
|
|
98
|
+
usedFallback,
|
|
99
|
+
ssml: generated.ssml,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
speechParts.push(spokenText);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
segments,
|
|
106
|
+
locale: normalizeLocale(args.language),
|
|
107
|
+
visibleText,
|
|
108
|
+
plainSpeechText: normalizeTextForSpeech(speechParts.join(" ")),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=assemble-plan.js.map
|