@pie-players/pie-assessment-toolkit 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +5 -0
  2. package/dist/components/PieAssessmentToolkit.custom-element.js +142 -85
  3. package/dist/context/assessment-toolkit-context.d.ts +8 -8
  4. package/dist/context/assessment-toolkit-context.d.ts.map +1 -1
  5. package/dist/index.d.ts +4 -4
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -2
  8. package/dist/services/HighlightCoordinator.d.ts +2 -2
  9. package/dist/services/HighlightCoordinator.d.ts.map +1 -1
  10. package/dist/services/HighlightCoordinator.js +1 -1
  11. package/dist/services/HighlightCoordinator.js.map +1 -1
  12. package/dist/services/I18nService.d.ts +3 -3
  13. package/dist/services/I18nService.d.ts.map +1 -1
  14. package/dist/services/PNPToolResolver.d.ts +7 -7
  15. package/dist/services/PNPToolResolver.js +6 -6
  16. package/dist/services/TTSService.d.ts +21 -9
  17. package/dist/services/TTSService.d.ts.map +1 -1
  18. package/dist/services/TTSService.js +172 -173
  19. package/dist/services/TTSService.js.map +1 -1
  20. package/dist/services/ThemeProvider.d.ts +2 -2
  21. package/dist/services/ThemeProvider.d.ts.map +1 -1
  22. package/dist/services/ToolCoordinator.d.ts +2 -2
  23. package/dist/services/ToolCoordinator.d.ts.map +1 -1
  24. package/dist/services/ToolRegistry.d.ts +7 -7
  25. package/dist/services/ToolRegistry.d.ts.map +1 -1
  26. package/dist/services/ToolkitCoordinator.d.ts +19 -3
  27. package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
  28. package/dist/services/ToolkitCoordinator.js +228 -214
  29. package/dist/services/ToolkitCoordinator.js.map +1 -1
  30. package/dist/services/interfaces.d.ts +20 -20
  31. package/dist/services/interfaces.d.ts.map +1 -1
  32. package/dist/services/interfaces.js +1 -1
  33. package/dist/services/interfaces.js.map +1 -1
  34. package/dist/services/pnp-provenance.d.ts +7 -7
  35. package/dist/services/pnp-provenance.js +1 -1
  36. package/dist/services/tool-providers/DesmosToolProvider.d.ts +3 -3
  37. package/dist/services/tool-providers/DesmosToolProvider.d.ts.map +1 -1
  38. package/dist/services/tool-providers/DesmosToolProvider.js +1 -1
  39. package/dist/services/tool-providers/TTSToolProvider.d.ts +3 -3
  40. package/dist/services/tool-providers/TTSToolProvider.d.ts.map +1 -1
  41. package/dist/services/tool-providers/TTSToolProvider.js +1 -1
  42. package/dist/services/tool-providers/ToolProviderApi.d.ts +26 -0
  43. package/dist/services/tool-providers/ToolProviderApi.d.ts.map +1 -0
  44. package/dist/services/tool-providers/ToolProviderApi.js +8 -0
  45. package/dist/services/tool-providers/ToolProviderApi.js.map +1 -0
  46. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +3 -3
  47. package/dist/services/tool-providers/ToolProviderRegistry.d.ts.map +1 -1
  48. package/dist/services/tool-providers/ToolProviderRegistry.js.map +1 -1
  49. package/dist/services/tool-providers/index.d.ts +1 -1
  50. package/dist/services/tool-providers/index.d.ts.map +1 -1
  51. package/dist/services/tts/browser-provider.d.ts.map +1 -1
  52. package/dist/services/tts/browser-provider.js +6 -37
  53. package/dist/services/tts/browser-provider.js.map +1 -1
  54. package/dist/services/tts/text-segmentation.d.ts +10 -0
  55. package/dist/services/tts/text-segmentation.d.ts.map +1 -0
  56. package/dist/services/tts/text-segmentation.js +38 -0
  57. package/dist/services/tts/text-segmentation.js.map +1 -0
  58. package/dist/services/tts-runtime-config.d.ts +31 -0
  59. package/dist/services/tts-runtime-config.d.ts.map +1 -0
  60. package/dist/services/tts-runtime-config.js +54 -0
  61. package/dist/services/tts-runtime-config.js.map +1 -0
  62. package/dist/tools/registrations/tts.d.ts.map +1 -1
  63. package/dist/tools/registrations/tts.js +6 -57
  64. package/dist/tools/registrations/tts.js.map +1 -1
  65. package/package.json +7 -7
  66. package/dist/services/tool-providers/IToolProvider.d.ts +0 -136
  67. package/dist/services/tool-providers/IToolProvider.d.ts.map +0 -1
  68. package/dist/services/tool-providers/IToolProvider.js +0 -12
  69. 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 { PNPProvenanceBuilder, } from "./pnp-provenance.js";
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 PNPToolResolver {
49
+ export class PnpToolResolver {
50
50
  toolRegistry;
51
51
  enableProvenance;
52
52
  /**
53
- * Create a PNPToolResolver
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 PNPToolResolver(registry);
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 PNPToolResolver(registry);
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 PNPProvenanceBuilder(contextId)
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 { IHighlightCoordinator } from "./interfaces.js";
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: IHighlightCoordinator): void;
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
- catalogId?: string;
126
- language?: string;
127
- contentElement?: Element;
128
- wordBoundaryOffset?: number;
129
- highlightModeOverride?: HighlightMode;
130
- }): Promise<void>;
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":"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"}
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 = new Map();
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
- try {
360
- const Segmenter = globalThis.Intl?.Segmenter;
361
- if (typeof Segmenter === "function") {
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
- if (options?.language) {
538
- const providerOptions = (this.ttsConfig.providerOptions ||
539
- {});
540
- const textNormalization = (providerOptions.textNormalization ||
541
- {});
542
- const segmenter = (providerOptions.segmenter || {});
543
- const mergedProviderOptions = {
544
- ...providerOptions,
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
- textNormalization: {
547
- ...textNormalization,
548
- locale: options.language,
549
- },
550
- segmenter: {
551
- ...segmenter,
552
- locale: options.language,
553
- },
554
- };
555
- this.ttsConfig = {
556
- ...this.ttsConfig,
557
- providerOptions: mergedProviderOptions,
558
- };
559
- if ("updateSettings" in this.provider &&
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
- // Normalize input text and prefer visible DOM-derived text when available.
566
- // This keeps generated speech aligned to what learners can actually see.
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
- const preview = contentToSpeak.replace(/\s+/g, " ").trim().slice(0, 200);
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: options?.catalogId || null,
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; // Clear previous error
645
+ this.lastError = null;
603
646
  this.currentBoundaryOffset = 0;
604
647
  this.currentSeekSegmentIndex = 0;
605
- const highlightMode = options?.highlightModeOverride || this.resolveHighlightMode();
606
- this.activeHighlightMode = highlightMode;
607
- const shouldUsePlan = !!this.currentContentElement &&
608
- !usedCatalogSpoken &&
609
- !this.hasExplicitBreakSemantics(contentToSpeak);
610
- this.seekSegments = this.hasExplicitBreakSemantics(contentToSpeak)
611
- ? []
612
- : shouldUsePlan && this.currentContentElement
613
- ? this.createSpeechPlan(this.currentContentElement, normalizedText)
614
- : this.createSeekSegmentsFromText(contentToSpeak);
615
- this.setState(PlaybackState.LOADING);
616
- // Build position map for highlighting if we have a content element
617
- if (this.currentContentElement && this.highlightCoordinator) {
618
- this.buildPositionMap(this.currentContentElement, contentToSpeak, options?.language);
619
- // Fallback sentence-level highlight for non-planned flows.
620
- // Planned sentence mode is updated progressively in speakWithPlan().
621
- if (!(highlightMode === "sentence" && shouldUsePlan)) {
622
- const range = document.createRange();
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
- try {
629
- // Setup word boundary highlighting
630
- if (highlightMode === "word" &&
631
- this.highlightCoordinator &&
632
- this.currentContentElement) {
633
- this.provider.onWordBoundary = (word, charIndex, length) => {
634
- const wordLength = length || word.length;
635
- const globalIndex = charIndex +
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
- await this.provider.speak(contentToSpeak);
677
+ console.warn(`[TTSService] Could not find highlight range for position ${globalIndex}, length ${wordLength}`);
661
678
  }
662
- if (runId !== this.speakRunId)
663
- return;
664
- this.setState(PlaybackState.IDLE);
665
- // Clear highlights when done
666
- if (this.highlightCoordinator) {
667
- this.highlightCoordinator.clearTTS();
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
- // Clear tracking
670
- this.currentContentElement = null;
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
- // Clear tracking
687
- this.currentContentElement = null;
688
- this.normalizedToDOM.clear();
689
- this.currentBoundaryOffset = 0;
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
- if (this.highlightCoordinator) {
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
- if (this.highlightCoordinator) {
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
  }