@pie-players/pie-assessment-toolkit 0.3.10 → 0.3.12
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 +5 -0
- package/dist/components/PieAssessmentToolkit.custom-element.js +142 -85
- package/dist/context/assessment-toolkit-context.d.ts +8 -8
- package/dist/context/assessment-toolkit-context.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/services/HighlightCoordinator.d.ts +2 -2
- package/dist/services/HighlightCoordinator.d.ts.map +1 -1
- package/dist/services/HighlightCoordinator.js +1 -1
- package/dist/services/HighlightCoordinator.js.map +1 -1
- package/dist/services/I18nService.d.ts +3 -3
- package/dist/services/I18nService.d.ts.map +1 -1
- package/dist/services/PNPToolResolver.d.ts +7 -7
- package/dist/services/PNPToolResolver.js +6 -6
- package/dist/services/TTSService.d.ts +21 -9
- package/dist/services/TTSService.d.ts.map +1 -1
- package/dist/services/TTSService.js +172 -173
- package/dist/services/TTSService.js.map +1 -1
- package/dist/services/ThemeProvider.d.ts +2 -2
- package/dist/services/ThemeProvider.d.ts.map +1 -1
- package/dist/services/ToolCoordinator.d.ts +2 -2
- package/dist/services/ToolCoordinator.d.ts.map +1 -1
- package/dist/services/ToolRegistry.d.ts +7 -7
- package/dist/services/ToolRegistry.d.ts.map +1 -1
- package/dist/services/ToolkitCoordinator.d.ts +21 -5
- package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
- package/dist/services/ToolkitCoordinator.js +229 -214
- package/dist/services/ToolkitCoordinator.js.map +1 -1
- package/dist/services/interfaces.d.ts +20 -20
- package/dist/services/interfaces.d.ts.map +1 -1
- package/dist/services/interfaces.js +1 -1
- package/dist/services/interfaces.js.map +1 -1
- package/dist/services/pnp-provenance.d.ts +7 -7
- package/dist/services/pnp-provenance.js +1 -1
- package/dist/services/section-controller-types.d.ts +15 -1
- package/dist/services/section-controller-types.d.ts.map +1 -1
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +3 -3
- package/dist/services/tool-providers/DesmosToolProvider.d.ts.map +1 -1
- package/dist/services/tool-providers/DesmosToolProvider.js +1 -1
- package/dist/services/tool-providers/TTSToolProvider.d.ts +3 -3
- package/dist/services/tool-providers/TTSToolProvider.d.ts.map +1 -1
- package/dist/services/tool-providers/TTSToolProvider.js +1 -1
- package/dist/services/tool-providers/ToolProviderApi.d.ts +26 -0
- package/dist/services/tool-providers/ToolProviderApi.d.ts.map +1 -0
- package/dist/services/tool-providers/ToolProviderApi.js +8 -0
- package/dist/services/tool-providers/ToolProviderApi.js.map +1 -0
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +3 -3
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts.map +1 -1
- package/dist/services/tool-providers/ToolProviderRegistry.js.map +1 -1
- package/dist/services/tool-providers/index.d.ts +1 -1
- package/dist/services/tool-providers/index.d.ts.map +1 -1
- package/dist/services/tts/browser-provider.d.ts.map +1 -1
- package/dist/services/tts/browser-provider.js +6 -37
- package/dist/services/tts/browser-provider.js.map +1 -1
- package/dist/services/tts/text-segmentation.d.ts +10 -0
- package/dist/services/tts/text-segmentation.d.ts.map +1 -0
- package/dist/services/tts/text-segmentation.js +38 -0
- package/dist/services/tts/text-segmentation.js.map +1 -0
- package/dist/services/tts-runtime-config.d.ts +31 -0
- package/dist/services/tts-runtime-config.d.ts.map +1 -0
- package/dist/services/tts-runtime-config.js +54 -0
- package/dist/services/tts-runtime-config.js.map +1 -0
- package/dist/tools/registrations/tts.d.ts.map +1 -1
- package/dist/tools/registrations/tts.js +6 -57
- package/dist/tools/registrations/tts.js.map +1 -1
- package/package.json +7 -7
- package/dist/services/tool-providers/IToolProvider.d.ts +0 -136
- package/dist/services/tool-providers/IToolProvider.d.ts.map +0 -1
- package/dist/services/tool-providers/IToolProvider.js +0 -12
- package/dist/services/tool-providers/IToolProvider.js.map +0 -1
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
*
|
|
43
43
|
* Part of PIE Assessment Toolkit.
|
|
44
44
|
*/
|
|
45
|
-
import {
|
|
45
|
+
import { PnpProvenanceBuilder, } from "./pnp-provenance.js";
|
|
46
46
|
/**
|
|
47
47
|
* Resolves tool availability from QTI 3.0 assessment structure
|
|
48
48
|
*/
|
|
49
|
-
export class
|
|
49
|
+
export class PnpToolResolver {
|
|
50
50
|
toolRegistry;
|
|
51
51
|
enableProvenance;
|
|
52
52
|
/**
|
|
53
|
-
* Create a
|
|
53
|
+
* Create a PnpToolResolver
|
|
54
54
|
*
|
|
55
55
|
* @param toolRegistry Tool registry for PNP support mapping
|
|
56
56
|
* @param enableProvenance Enable detailed provenance tracking (default: true)
|
|
@@ -67,7 +67,7 @@ export class PNPToolResolver {
|
|
|
67
67
|
* @returns Array of resolved tool configurations
|
|
68
68
|
*
|
|
69
69
|
* @example
|
|
70
|
-
* const resolver = new
|
|
70
|
+
* const resolver = new PnpToolResolver(registry);
|
|
71
71
|
* const tools = resolver.resolveTools(assessment, itemRef);
|
|
72
72
|
* tools.forEach(tool => {
|
|
73
73
|
* console.log(`${tool.id}: ${tool.enabled ? 'enabled' : 'disabled'}`);
|
|
@@ -91,7 +91,7 @@ export class PNPToolResolver {
|
|
|
91
91
|
* @returns Resolution result with tools and provenance
|
|
92
92
|
*
|
|
93
93
|
* @example
|
|
94
|
-
* const resolver = new
|
|
94
|
+
* const resolver = new PnpToolResolver(registry);
|
|
95
95
|
* const result = resolver.resolveToolsWithProvenance(assessment, itemRef);
|
|
96
96
|
*
|
|
97
97
|
* // Use resolved tools
|
|
@@ -110,7 +110,7 @@ export class PNPToolResolver {
|
|
|
110
110
|
// Initialize provenance builder
|
|
111
111
|
const contextId = `assessment-${assessment.id || "unknown"}${currentItemRef ? `-item-${currentItemRef.identifier}` : ""}`;
|
|
112
112
|
const provenanceBuilder = this.enableProvenance
|
|
113
|
-
? new
|
|
113
|
+
? new PnpProvenanceBuilder(contextId)
|
|
114
114
|
: null;
|
|
115
115
|
// Record configuration sources
|
|
116
116
|
if (provenanceBuilder) {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { ITTSProvider, TTSConfig, TTSProviderCapabilities } from "@pie-players/pie-tts";
|
|
19
19
|
import type { AccessibilityCatalogResolver } from "./AccessibilityCatalogResolver.js";
|
|
20
|
-
import type {
|
|
20
|
+
import type { HighlightCoordinatorApi } from "./interfaces.js";
|
|
21
21
|
export type { ITTSProvider, ITTSProviderImplementation, TTSConfig, TTSFeature, TTSProviderCapabilities, } from "@pie-players/pie-tts";
|
|
22
22
|
/**
|
|
23
23
|
* Playback state
|
|
@@ -30,6 +30,13 @@ export declare enum PlaybackState {
|
|
|
30
30
|
ERROR = "error"
|
|
31
31
|
}
|
|
32
32
|
type HighlightMode = "word" | "sentence";
|
|
33
|
+
interface SpeakOptions {
|
|
34
|
+
catalogId?: string;
|
|
35
|
+
language?: string;
|
|
36
|
+
contentElement?: Element;
|
|
37
|
+
wordBoundaryOffset?: number;
|
|
38
|
+
highlightModeOverride?: HighlightMode;
|
|
39
|
+
}
|
|
33
40
|
/**
|
|
34
41
|
* TTSService
|
|
35
42
|
*
|
|
@@ -73,7 +80,7 @@ export declare class TTSService {
|
|
|
73
80
|
/**
|
|
74
81
|
* Set highlight coordinator for word highlighting
|
|
75
82
|
*/
|
|
76
|
-
setHighlightCoordinator(coordinator:
|
|
83
|
+
setHighlightCoordinator(coordinator: HighlightCoordinatorApi): void;
|
|
77
84
|
/**
|
|
78
85
|
* Set accessibility catalog resolver for spoken content
|
|
79
86
|
*
|
|
@@ -102,6 +109,9 @@ export declare class TTSService {
|
|
|
102
109
|
private getBoundaryStrength;
|
|
103
110
|
private getBoundaryAnchor;
|
|
104
111
|
private createSpeechPlan;
|
|
112
|
+
private collectSpeechPlanBoundaries;
|
|
113
|
+
private createNodeStartOffsets;
|
|
114
|
+
private createSpeechPlanSegments;
|
|
105
115
|
private segmentSentences;
|
|
106
116
|
private createSeekSegmentsFromText;
|
|
107
117
|
private getCurrentSeekSegmentIndex;
|
|
@@ -121,13 +131,15 @@ export declare class TTSService {
|
|
|
121
131
|
* @param text Text to speak (will be normalized: trimmed and whitespace collapsed)
|
|
122
132
|
* @param options Optional catalog ID, language, and content element for highlighting
|
|
123
133
|
*/
|
|
124
|
-
speak(text: string, options?:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
speak(text: string, options?: SpeakOptions): Promise<void>;
|
|
135
|
+
private applyLanguageSettings;
|
|
136
|
+
private resolveSpeechContent;
|
|
137
|
+
private logResolvedSpeechContent;
|
|
138
|
+
private initializeSpeakTracking;
|
|
139
|
+
private prepareHighlightsForSpeak;
|
|
140
|
+
private configureWordBoundaryHighlighting;
|
|
141
|
+
private executeSpeakPlayback;
|
|
142
|
+
private clearHighlightsAndTracking;
|
|
131
143
|
/**
|
|
132
144
|
* Speak a text range with accurate word highlighting
|
|
133
145
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TTSService.d.ts","sourceRoot":"","sources":["../../src/services/TTSService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EACX,YAAY,EAEZ,SAAS,EACT,uBAAuB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACtF,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"TTSService.d.ts","sourceRoot":"","sources":["../../src/services/TTSService.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* Part of PIE Assessment Toolkit.
|
|
17
17
|
*/
|
|
18
18
|
import { collectVisibleTextAndMap, extractVisibleText as extractVisibleTextFromDOM, isElementHiddenForTTS, normalizeTextForSpeech, } from "./tts/text-processing.js";
|
|
19
|
+
import { segmentSentences as segmentTextToSentences, } from "./tts/text-segmentation.js";
|
|
19
20
|
/**
|
|
20
21
|
* Playback state
|
|
21
22
|
*/
|
|
@@ -295,17 +296,16 @@ export class TTSService {
|
|
|
295
296
|
};
|
|
296
297
|
}
|
|
297
298
|
createSpeechPlan(contentElement, normalizedText) {
|
|
299
|
+
const boundaries = this.collectSpeechPlanBoundaries(contentElement, normalizedText);
|
|
300
|
+
return this.createSpeechPlanSegments(normalizedText, boundaries);
|
|
301
|
+
}
|
|
302
|
+
collectSpeechPlanBoundaries(contentElement, normalizedText) {
|
|
298
303
|
const boundaries = new Map();
|
|
299
|
-
let previousBoundaryAnchor = null;
|
|
300
304
|
const { map } = collectVisibleTextAndMap(contentElement, this.getTextProcessingOptions());
|
|
301
|
-
const nodeStartOffsets =
|
|
302
|
-
for (const [normalizedIndex, mapping] of map.entries()) {
|
|
303
|
-
if (!nodeStartOffsets.has(mapping.node)) {
|
|
304
|
-
nodeStartOffsets.set(mapping.node, normalizedIndex);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
305
|
+
const nodeStartOffsets = this.createNodeStartOffsets(map);
|
|
307
306
|
const walker = document.createTreeWalker(contentElement, NodeFilter.SHOW_TEXT);
|
|
308
307
|
let currentNode = walker.nextNode();
|
|
308
|
+
let previousBoundaryAnchor = null;
|
|
309
309
|
while (currentNode) {
|
|
310
310
|
const textNode = currentNode;
|
|
311
311
|
const parent = textNode.parentElement;
|
|
@@ -322,6 +322,18 @@ export class TTSService {
|
|
|
322
322
|
}
|
|
323
323
|
currentNode = walker.nextNode();
|
|
324
324
|
}
|
|
325
|
+
return boundaries;
|
|
326
|
+
}
|
|
327
|
+
createNodeStartOffsets(map) {
|
|
328
|
+
const nodeStartOffsets = new Map();
|
|
329
|
+
for (const [normalizedIndex, mapping] of map.entries()) {
|
|
330
|
+
if (!nodeStartOffsets.has(mapping.node)) {
|
|
331
|
+
nodeStartOffsets.set(mapping.node, normalizedIndex);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return nodeStartOffsets;
|
|
335
|
+
}
|
|
336
|
+
createSpeechPlanSegments(normalizedText, boundaries) {
|
|
325
337
|
const points = Array.from(boundaries.keys())
|
|
326
338
|
.filter((point) => point > 0 && point < normalizedText.length)
|
|
327
339
|
.sort((a, b) => a - b);
|
|
@@ -356,39 +368,9 @@ export class TTSService {
|
|
|
356
368
|
return segments.filter((segment) => segment.text.trim().length > 0);
|
|
357
369
|
}
|
|
358
370
|
segmentSentences(text) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const locale = this.ttsConfig.providerOptions
|
|
363
|
-
?.locale || undefined;
|
|
364
|
-
const segmenter = new Segmenter(locale, {
|
|
365
|
-
granularity: "sentence",
|
|
366
|
-
});
|
|
367
|
-
const segments = Array.from(segmenter.segment(text))
|
|
368
|
-
.map((segment) => ({
|
|
369
|
-
text: segment.segment,
|
|
370
|
-
offset: segment.index,
|
|
371
|
-
}))
|
|
372
|
-
.filter((segment) => segment.text.trim().length > 0);
|
|
373
|
-
if (segments.length > 0)
|
|
374
|
-
return segments;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
// Fall through to regex segmentation.
|
|
379
|
-
}
|
|
380
|
-
const sentenceRegex = /[^.!?]+(?:[.!?]+|$)/g;
|
|
381
|
-
const raw = text.match(sentenceRegex) || [text];
|
|
382
|
-
const parsed = [];
|
|
383
|
-
let processed = 0;
|
|
384
|
-
for (const sentence of raw) {
|
|
385
|
-
const offset = text.indexOf(sentence, processed);
|
|
386
|
-
if (offset === -1)
|
|
387
|
-
continue;
|
|
388
|
-
parsed.push({ text: sentence, offset });
|
|
389
|
-
processed = offset + sentence.length;
|
|
390
|
-
}
|
|
391
|
-
return parsed.length > 0 ? parsed : [{ text, offset: 0 }];
|
|
371
|
+
const locale = this.ttsConfig.providerOptions
|
|
372
|
+
?.locale || undefined;
|
|
373
|
+
return segmentTextToSentences(text, { locale });
|
|
392
374
|
}
|
|
393
375
|
createSeekSegmentsFromText(text) {
|
|
394
376
|
return this.segmentSentences(text)
|
|
@@ -534,42 +516,94 @@ export class TTSService {
|
|
|
534
516
|
throw new Error("TTS service not initialized");
|
|
535
517
|
}
|
|
536
518
|
const runId = ++this.speakRunId;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
519
|
+
await this.applyLanguageSettings(options);
|
|
520
|
+
const { contentToSpeak, normalizedText, usedCatalogSpoken, speechSource } = this.resolveSpeechContent(text, options);
|
|
521
|
+
this.logResolvedSpeechContent({
|
|
522
|
+
contentToSpeak,
|
|
523
|
+
speechSource,
|
|
524
|
+
catalogId: options?.catalogId,
|
|
525
|
+
});
|
|
526
|
+
this.initializeSpeakTracking(contentToSpeak, options);
|
|
527
|
+
const highlightMode = options?.highlightModeOverride || this.resolveHighlightMode();
|
|
528
|
+
this.activeHighlightMode = highlightMode;
|
|
529
|
+
const shouldUsePlan = !!this.currentContentElement &&
|
|
530
|
+
!usedCatalogSpoken &&
|
|
531
|
+
!this.hasExplicitBreakSemantics(contentToSpeak);
|
|
532
|
+
this.seekSegments = this.hasExplicitBreakSemantics(contentToSpeak)
|
|
533
|
+
? []
|
|
534
|
+
: shouldUsePlan && this.currentContentElement
|
|
535
|
+
? this.createSpeechPlan(this.currentContentElement, normalizedText)
|
|
536
|
+
: this.createSeekSegmentsFromText(contentToSpeak);
|
|
537
|
+
this.setState(PlaybackState.LOADING);
|
|
538
|
+
this.prepareHighlightsForSpeak({
|
|
539
|
+
contentToSpeak,
|
|
540
|
+
options,
|
|
541
|
+
highlightMode,
|
|
542
|
+
shouldUsePlan,
|
|
543
|
+
});
|
|
544
|
+
try {
|
|
545
|
+
this.configureWordBoundaryHighlighting({
|
|
546
|
+
highlightMode,
|
|
547
|
+
wordBoundaryOffset: options?.wordBoundaryOffset || 0,
|
|
548
|
+
});
|
|
549
|
+
this.setState(PlaybackState.PLAYING);
|
|
550
|
+
await this.executeSpeakPlayback({
|
|
551
|
+
shouldUsePlan,
|
|
552
|
+
runId,
|
|
553
|
+
highlightMode,
|
|
554
|
+
contentToSpeak,
|
|
555
|
+
});
|
|
556
|
+
if (runId !== this.speakRunId)
|
|
557
|
+
return;
|
|
558
|
+
this.setState(PlaybackState.IDLE);
|
|
559
|
+
this.clearHighlightsAndTracking();
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
console.error("TTS error:", error);
|
|
563
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
564
|
+
if (runId !== this.speakRunId)
|
|
565
|
+
return;
|
|
566
|
+
this.setState(PlaybackState.ERROR);
|
|
567
|
+
this.clearHighlightsAndTracking();
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async applyLanguageSettings(options) {
|
|
572
|
+
if (!options?.language || !this.provider)
|
|
573
|
+
return;
|
|
574
|
+
const providerOptions = (this.ttsConfig.providerOptions ||
|
|
575
|
+
{});
|
|
576
|
+
const textNormalization = (providerOptions.textNormalization ||
|
|
577
|
+
{});
|
|
578
|
+
const segmenter = (providerOptions.segmenter || {});
|
|
579
|
+
const mergedProviderOptions = {
|
|
580
|
+
...providerOptions,
|
|
581
|
+
locale: options.language,
|
|
582
|
+
textNormalization: {
|
|
583
|
+
...textNormalization,
|
|
545
584
|
locale: options.language,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
typeof this.provider.updateSettings ===
|
|
561
|
-
"function") {
|
|
562
|
-
await this.provider.updateSettings({ providerOptions: mergedProviderOptions });
|
|
563
|
-
}
|
|
585
|
+
},
|
|
586
|
+
segmenter: {
|
|
587
|
+
...segmenter,
|
|
588
|
+
locale: options.language,
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
this.ttsConfig = {
|
|
592
|
+
...this.ttsConfig,
|
|
593
|
+
providerOptions: mergedProviderOptions,
|
|
594
|
+
};
|
|
595
|
+
if ("updateSettings" in this.provider &&
|
|
596
|
+
typeof this.provider.updateSettings ===
|
|
597
|
+
"function") {
|
|
598
|
+
await this.provider.updateSettings({ providerOptions: mergedProviderOptions });
|
|
564
599
|
}
|
|
565
|
-
|
|
566
|
-
|
|
600
|
+
}
|
|
601
|
+
resolveSpeechContent(text, options) {
|
|
567
602
|
const normalizedInputText = normalizeTextForSpeech(text);
|
|
568
603
|
const normalizedText = options?.contentElement
|
|
569
604
|
? this.extractVisibleText(options.contentElement, options.language) ||
|
|
570
605
|
normalizedInputText
|
|
571
606
|
: normalizedInputText;
|
|
572
|
-
// Try to resolve from accessibility catalog if catalogId provided
|
|
573
607
|
let contentToSpeak = normalizedText;
|
|
574
608
|
let usedCatalogSpoken = false;
|
|
575
609
|
let speechSource = "dom-or-input";
|
|
@@ -580,7 +614,6 @@ export class TTSService {
|
|
|
580
614
|
useFallback: true,
|
|
581
615
|
});
|
|
582
616
|
if (catalogContent) {
|
|
583
|
-
// Use pre-authored spoken content from catalog
|
|
584
617
|
contentToSpeak = catalogContent.content;
|
|
585
618
|
usedCatalogSpoken = true;
|
|
586
619
|
speechSource = "catalog-spoken";
|
|
@@ -590,108 +623,88 @@ export class TTSService {
|
|
|
590
623
|
console.debug(`[TTSService] No catalog found for "${options.catalogId}", falling back to generated TTS`);
|
|
591
624
|
}
|
|
592
625
|
}
|
|
593
|
-
|
|
626
|
+
return {
|
|
627
|
+
contentToSpeak,
|
|
628
|
+
usedCatalogSpoken,
|
|
629
|
+
speechSource,
|
|
630
|
+
normalizedText,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
logResolvedSpeechContent(args) {
|
|
634
|
+
const preview = args.contentToSpeak.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
594
635
|
console.debug("[TTSService] Speak resolved content", {
|
|
595
|
-
source: speechSource,
|
|
596
|
-
catalogId:
|
|
597
|
-
length: contentToSpeak.length,
|
|
636
|
+
source: args.speechSource,
|
|
637
|
+
catalogId: args.catalogId || null,
|
|
638
|
+
length: args.contentToSpeak.length,
|
|
598
639
|
preview,
|
|
599
640
|
});
|
|
641
|
+
}
|
|
642
|
+
initializeSpeakTracking(contentToSpeak, options) {
|
|
600
643
|
this.currentText = contentToSpeak;
|
|
601
644
|
this.currentContentElement = options?.contentElement || null;
|
|
602
|
-
this.lastError = null;
|
|
645
|
+
this.lastError = null;
|
|
603
646
|
this.currentBoundaryOffset = 0;
|
|
604
647
|
this.currentSeekSegmentIndex = 0;
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
range.selectNodeContents(this.currentContentElement);
|
|
624
|
-
this.highlightCoordinator.highlightTTSSentence([range]);
|
|
625
|
-
console.log("[TTSService] Applied sentence-level highlighting");
|
|
626
|
-
}
|
|
648
|
+
}
|
|
649
|
+
prepareHighlightsForSpeak(args) {
|
|
650
|
+
if (!this.currentContentElement || !this.highlightCoordinator)
|
|
651
|
+
return;
|
|
652
|
+
this.buildPositionMap(this.currentContentElement, args.contentToSpeak, args.options?.language);
|
|
653
|
+
if (!(args.highlightMode === "sentence" && args.shouldUsePlan)) {
|
|
654
|
+
const range = document.createRange();
|
|
655
|
+
range.selectNodeContents(this.currentContentElement);
|
|
656
|
+
this.highlightCoordinator.highlightTTSSentence([range]);
|
|
657
|
+
console.log("[TTSService] Applied sentence-level highlighting");
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
configureWordBoundaryHighlighting(args) {
|
|
661
|
+
if (!this.provider ||
|
|
662
|
+
args.highlightMode !== "word" ||
|
|
663
|
+
!this.highlightCoordinator ||
|
|
664
|
+
!this.currentContentElement) {
|
|
665
|
+
return;
|
|
627
666
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
this.currentBoundaryOffset +
|
|
637
|
-
(options?.wordBoundaryOffset || 0);
|
|
638
|
-
const highlightRange = this.findHighlightRange(globalIndex, wordLength);
|
|
639
|
-
if (highlightRange && this.highlightCoordinator) {
|
|
640
|
-
const highlightText = highlightRange.node.textContent?.substring(highlightRange.start, highlightRange.end) || "";
|
|
641
|
-
console.log(`[TTSService] Highlighting "${highlightText}" (word: "${word}") at position ${globalIndex}`);
|
|
642
|
-
this.highlightCoordinator.highlightTTSWord(highlightRange.node, highlightRange.start, highlightRange.end);
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
console.warn(`[TTSService] Could not find highlight range for position ${globalIndex}, length ${wordLength}`);
|
|
646
|
-
}
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
this.setState(PlaybackState.PLAYING);
|
|
650
|
-
if (shouldUsePlan && this.currentContentElement) {
|
|
651
|
-
const segments = this.seekSegments;
|
|
652
|
-
if (segments.length > 0) {
|
|
653
|
-
await this.speakWithPlan(segments, runId, { highlightMode });
|
|
654
|
-
}
|
|
655
|
-
else {
|
|
656
|
-
await this.provider.speak(contentToSpeak);
|
|
657
|
-
}
|
|
667
|
+
this.provider.onWordBoundary = (word, charIndex, length) => {
|
|
668
|
+
const wordLength = length || word.length;
|
|
669
|
+
const globalIndex = charIndex + this.currentBoundaryOffset + args.wordBoundaryOffset;
|
|
670
|
+
const highlightRange = this.findHighlightRange(globalIndex, wordLength);
|
|
671
|
+
if (highlightRange && this.highlightCoordinator) {
|
|
672
|
+
const highlightText = highlightRange.node.textContent?.substring(highlightRange.start, highlightRange.end) || "";
|
|
673
|
+
console.log(`[TTSService] Highlighting "${highlightText}" (word: "${word}") at position ${globalIndex}`);
|
|
674
|
+
this.highlightCoordinator.highlightTTSWord(highlightRange.node, highlightRange.start, highlightRange.end);
|
|
658
675
|
}
|
|
659
676
|
else {
|
|
660
|
-
|
|
677
|
+
console.warn(`[TTSService] Could not find highlight range for position ${globalIndex}, length ${wordLength}`);
|
|
661
678
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
async executeSpeakPlayback(args) {
|
|
682
|
+
if (args.shouldUsePlan && this.currentContentElement) {
|
|
683
|
+
const segments = this.seekSegments;
|
|
684
|
+
if (segments.length > 0) {
|
|
685
|
+
await this.speakWithPlan(segments, args.runId, {
|
|
686
|
+
highlightMode: args.highlightMode,
|
|
687
|
+
});
|
|
668
688
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
this.normalizedToDOM.clear();
|
|
672
|
-
this.currentBoundaryOffset = 0;
|
|
673
|
-
this.seekSegments = [];
|
|
674
|
-
this.currentSeekSegmentIndex = 0;
|
|
675
|
-
}
|
|
676
|
-
catch (error) {
|
|
677
|
-
console.error("TTS error:", error);
|
|
678
|
-
this.lastError = error instanceof Error ? error.message : String(error);
|
|
679
|
-
if (runId !== this.speakRunId)
|
|
680
|
-
return;
|
|
681
|
-
this.setState(PlaybackState.ERROR);
|
|
682
|
-
// Clear highlights on error
|
|
683
|
-
if (this.highlightCoordinator) {
|
|
684
|
-
this.highlightCoordinator.clearTTS();
|
|
689
|
+
else if (this.provider) {
|
|
690
|
+
await this.provider.speak(args.contentToSpeak);
|
|
685
691
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
this.
|
|
690
|
-
this.seekSegments = [];
|
|
691
|
-
this.currentSeekSegmentIndex = 0;
|
|
692
|
-
throw error;
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (this.provider) {
|
|
695
|
+
await this.provider.speak(args.contentToSpeak);
|
|
693
696
|
}
|
|
694
697
|
}
|
|
698
|
+
clearHighlightsAndTracking() {
|
|
699
|
+
if (this.highlightCoordinator) {
|
|
700
|
+
this.highlightCoordinator.clearTTS();
|
|
701
|
+
}
|
|
702
|
+
this.currentContentElement = null;
|
|
703
|
+
this.normalizedToDOM.clear();
|
|
704
|
+
this.currentBoundaryOffset = 0;
|
|
705
|
+
this.seekSegments = [];
|
|
706
|
+
this.currentSeekSegmentIndex = 0;
|
|
707
|
+
}
|
|
695
708
|
/**
|
|
696
709
|
* Speak a text range with accurate word highlighting
|
|
697
710
|
*
|
|
@@ -790,28 +803,14 @@ export class TTSService {
|
|
|
790
803
|
if (runId !== this.speakRunId)
|
|
791
804
|
return;
|
|
792
805
|
this.setState(PlaybackState.IDLE);
|
|
793
|
-
|
|
794
|
-
this.highlightCoordinator.clearTTS();
|
|
795
|
-
}
|
|
796
|
-
this.currentContentElement = null;
|
|
797
|
-
this.normalizedToDOM.clear();
|
|
798
|
-
this.currentBoundaryOffset = 0;
|
|
799
|
-
this.seekSegments = [];
|
|
800
|
-
this.currentSeekSegmentIndex = 0;
|
|
806
|
+
this.clearHighlightsAndTracking();
|
|
801
807
|
}
|
|
802
808
|
catch (error) {
|
|
803
809
|
if (runId !== this.speakRunId)
|
|
804
810
|
return;
|
|
805
811
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
806
812
|
this.setState(PlaybackState.ERROR);
|
|
807
|
-
|
|
808
|
-
this.highlightCoordinator.clearTTS();
|
|
809
|
-
}
|
|
810
|
-
this.currentContentElement = null;
|
|
811
|
-
this.normalizedToDOM.clear();
|
|
812
|
-
this.currentBoundaryOffset = 0;
|
|
813
|
-
this.seekSegments = [];
|
|
814
|
-
this.currentSeekSegmentIndex = 0;
|
|
813
|
+
this.clearHighlightsAndTracking();
|
|
815
814
|
throw error;
|
|
816
815
|
}
|
|
817
816
|
}
|