@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.
Files changed (71) 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 +21 -5
  27. package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
  28. package/dist/services/ToolkitCoordinator.js +229 -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/section-controller-types.d.ts +15 -1
  37. package/dist/services/section-controller-types.d.ts.map +1 -1
  38. package/dist/services/tool-providers/DesmosToolProvider.d.ts +3 -3
  39. package/dist/services/tool-providers/DesmosToolProvider.d.ts.map +1 -1
  40. package/dist/services/tool-providers/DesmosToolProvider.js +1 -1
  41. package/dist/services/tool-providers/TTSToolProvider.d.ts +3 -3
  42. package/dist/services/tool-providers/TTSToolProvider.d.ts.map +1 -1
  43. package/dist/services/tool-providers/TTSToolProvider.js +1 -1
  44. package/dist/services/tool-providers/ToolProviderApi.d.ts +26 -0
  45. package/dist/services/tool-providers/ToolProviderApi.d.ts.map +1 -0
  46. package/dist/services/tool-providers/ToolProviderApi.js +8 -0
  47. package/dist/services/tool-providers/ToolProviderApi.js.map +1 -0
  48. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +3 -3
  49. package/dist/services/tool-providers/ToolProviderRegistry.d.ts.map +1 -1
  50. package/dist/services/tool-providers/ToolProviderRegistry.js.map +1 -1
  51. package/dist/services/tool-providers/index.d.ts +1 -1
  52. package/dist/services/tool-providers/index.d.ts.map +1 -1
  53. package/dist/services/tts/browser-provider.d.ts.map +1 -1
  54. package/dist/services/tts/browser-provider.js +6 -37
  55. package/dist/services/tts/browser-provider.js.map +1 -1
  56. package/dist/services/tts/text-segmentation.d.ts +10 -0
  57. package/dist/services/tts/text-segmentation.d.ts.map +1 -0
  58. package/dist/services/tts/text-segmentation.js +38 -0
  59. package/dist/services/tts/text-segmentation.js.map +1 -0
  60. package/dist/services/tts-runtime-config.d.ts +31 -0
  61. package/dist/services/tts-runtime-config.d.ts.map +1 -0
  62. package/dist/services/tts-runtime-config.js +54 -0
  63. package/dist/services/tts-runtime-config.js.map +1 -0
  64. package/dist/tools/registrations/tts.d.ts.map +1 -1
  65. package/dist/tools/registrations/tts.js +6 -57
  66. package/dist/tools/registrations/tts.js.map +1 -1
  67. package/package.json +7 -7
  68. package/dist/services/tool-providers/IToolProvider.d.ts +0 -136
  69. package/dist/services/tool-providers/IToolProvider.d.ts.map +0 -1
  70. package/dist/services/tool-providers/IToolProvider.js +0 -12
  71. package/dist/services/tool-providers/IToolProvider.js.map +0 -1
@@ -21,6 +21,7 @@ import { HighlightCoordinator } from "./HighlightCoordinator.js";
21
21
  import { ToolCoordinator } from "./ToolCoordinator.js";
22
22
  import { TTSService } from "./TTSService.js";
23
23
  import { BrowserTTSProvider } from "./tts/browser-provider.js";
24
+ import { buildRuntimeTTSConfig, resolveTTSBackend, } from "./tts-runtime-config.js";
24
25
  import { ToolProviderRegistry } from "./tool-providers/index.js";
25
26
  import { createPackagedToolRegistry } from "./createDefaultToolRegistry.js";
26
27
  const SECTION_ITEM_EVENT_TYPES = [
@@ -33,6 +34,7 @@ const SECTION_ITEM_EVENT_TYPES = [
33
34
  ];
34
35
  const SECTION_SCOPED_EVENT_TYPES = [
35
36
  "section-navigation-change",
37
+ "section-session-applied",
36
38
  "section-loading-complete",
37
39
  "section-items-complete-changed",
38
40
  "section-error",
@@ -376,9 +378,38 @@ export class ToolkitCoordinator {
376
378
  return this.sectionControllers.get(this.getSectionControllerMapKey(key));
377
379
  }
378
380
  subscribeSectionEvents(args) {
379
- let controllerEntry;
381
+ const controllerEntry = this.resolveSectionSubscriptionEntry(args);
382
+ if (!controllerEntry)
383
+ return () => { };
384
+ const { mapKey, controller } = controllerEntry;
385
+ const subscribe = controller.subscribe;
386
+ if (!subscribe) {
387
+ const resolvedKey = this.sectionControllerKeys.get(mapKey);
388
+ const sectionLabel = resolvedKey?.sectionId || args.sectionId || "<unknown>";
389
+ const attemptLabel = resolvedKey?.attemptId || args.attemptId || "<default>";
390
+ throw new Error(`[ToolkitCoordinator] subscribeSectionEvents could not subscribe: resolved controller for section "${sectionLabel}" attempt "${attemptLabel}" does not expose subscribe().`);
391
+ }
392
+ const listenerId = this.getOrCreateSectionEventListenerId(args.listener);
393
+ const subscriptionKey = `${mapKey}::${listenerId}`;
394
+ const previousSubscription = this.sectionEventSubscriptions.get(subscriptionKey);
395
+ previousSubscription?.();
396
+ const shouldDeliverEvent = this.buildSectionEventPredicate(args);
397
+ const unsubscribeController = subscribe.call(controller, (event) => {
398
+ if (!shouldDeliverEvent(event))
399
+ return;
400
+ args.listener(event);
401
+ });
402
+ const replayEvent = this.buildLoadingCompleteReplayEvent(controller);
403
+ if (replayEvent && shouldDeliverEvent(replayEvent)) {
404
+ args.listener(replayEvent);
405
+ }
406
+ const detach = this.createSectionEventDetachHandler(subscriptionKey, unsubscribeController);
407
+ this.sectionEventSubscriptions.set(subscriptionKey, detach);
408
+ return detach;
409
+ }
410
+ resolveSectionSubscriptionEntry(args) {
380
411
  try {
381
- controllerEntry = this.resolveSectionControllerForSubscription({
412
+ return this.resolveSectionControllerForSubscription({
382
413
  sectionId: args.sectionId,
383
414
  attemptId: args.attemptId,
384
415
  });
@@ -390,55 +421,30 @@ export class ToolkitCoordinator {
390
421
  message.includes("subscribeSectionEvents is ambiguous for section");
391
422
  if (isAmbiguousSectionWithoutAttempt) {
392
423
  console.warn(message);
393
- return () => { };
424
+ return null;
394
425
  }
395
426
  throw error;
396
427
  }
397
- const controller = controllerEntry.controller;
398
- const subscribe = controller.subscribe;
399
- if (!subscribe) {
400
- const resolvedKey = this.sectionControllerKeys.get(controllerEntry.mapKey);
401
- const sectionLabel = resolvedKey?.sectionId || args.sectionId || "<unknown>";
402
- const attemptLabel = resolvedKey?.attemptId || args.attemptId || "<default>";
403
- throw new Error(`[ToolkitCoordinator] subscribeSectionEvents could not subscribe: resolved controller for section "${sectionLabel}" attempt "${attemptLabel}" does not expose subscribe().`);
404
- }
405
- const { mapKey } = controllerEntry;
406
- let listenerId = this.sectionEventListenerIds.get(args.listener);
428
+ }
429
+ getOrCreateSectionEventListenerId(listener) {
430
+ let listenerId = this.sectionEventListenerIds.get(listener);
407
431
  if (!listenerId) {
408
432
  listenerId = this.nextSectionEventListenerId++;
409
- this.sectionEventListenerIds.set(args.listener, listenerId);
433
+ this.sectionEventListenerIds.set(listener, listenerId);
410
434
  }
411
- const subscriptionKey = `${mapKey}::${listenerId}`;
412
- const previousSubscription = this.sectionEventSubscriptions.get(subscriptionKey);
413
- previousSubscription?.();
414
- const eventTypeFilter = args.eventTypes
415
- ? new Set(args.eventTypes)
416
- : null;
435
+ return listenerId;
436
+ }
437
+ buildSectionEventPredicate(args) {
438
+ const eventTypeFilter = args.eventTypes ? new Set(args.eventTypes) : null;
417
439
  const itemIdFilter = args.itemIds ? new Set(args.itemIds) : null;
418
- const shouldDeliverEvent = (event) => {
440
+ return (event) => {
419
441
  if (eventTypeFilter || itemIdFilter) {
420
442
  const eventType = event?.type || null;
421
443
  if (eventTypeFilter && (!eventType || !eventTypeFilter.has(eventType))) {
422
444
  return false;
423
445
  }
424
446
  if (itemIdFilter) {
425
- const eventItemCandidates = new Set();
426
- if ("itemId" in event && typeof event.itemId === "string") {
427
- eventItemCandidates.add(event.itemId);
428
- }
429
- if ("canonicalItemId" in event &&
430
- typeof event.canonicalItemId === "string") {
431
- eventItemCandidates.add(event.canonicalItemId);
432
- }
433
- if ("currentItemId" in event &&
434
- typeof event.currentItemId === "string") {
435
- eventItemCandidates.add(event.currentItemId);
436
- }
437
- if ("previousItemId" in event &&
438
- typeof event.previousItemId === "string") {
439
- eventItemCandidates.add(event.previousItemId);
440
- }
441
- const hasMatchingItem = Array.from(eventItemCandidates).some((itemId) => itemIdFilter.has(itemId));
447
+ const hasMatchingItem = Array.from(this.collectEventItemIds(event)).some((itemId) => itemIdFilter.has(itemId));
442
448
  if (!hasMatchingItem) {
443
449
  return false;
444
450
  }
@@ -446,26 +452,44 @@ export class ToolkitCoordinator {
446
452
  }
447
453
  return true;
448
454
  };
449
- const unsubscribeController = subscribe.call(controller, (event) => {
450
- if (!shouldDeliverEvent(event))
451
- return;
452
- args.listener(event);
453
- });
454
- const runtimeState = controller.getRuntimeState?.();
455
- if (runtimeState?.loadingComplete &&
456
- runtimeState.totalRegistered > 0 &&
457
- runtimeState.totalLoaded >= runtimeState.totalRegistered) {
458
- const loadingCompleteEvent = {
459
- type: "section-loading-complete",
460
- totalRegistered: runtimeState.totalRegistered,
461
- totalLoaded: runtimeState.totalLoaded,
462
- currentItemIndex: runtimeState.currentItemIndex,
463
- timestamp: Date.now(),
464
- };
465
- if (shouldDeliverEvent(loadingCompleteEvent)) {
466
- args.listener(loadingCompleteEvent);
467
- }
455
+ }
456
+ collectEventItemIds(event) {
457
+ const itemIds = new Set();
458
+ if ("itemId" in event && typeof event.itemId === "string") {
459
+ itemIds.add(event.itemId);
460
+ }
461
+ if ("canonicalItemId" in event && typeof event.canonicalItemId === "string") {
462
+ itemIds.add(event.canonicalItemId);
463
+ }
464
+ if ("currentItemId" in event && typeof event.currentItemId === "string") {
465
+ itemIds.add(event.currentItemId);
468
466
  }
467
+ if ("previousItemId" in event && typeof event.previousItemId === "string") {
468
+ itemIds.add(event.previousItemId);
469
+ }
470
+ return itemIds;
471
+ }
472
+ buildLoadingCompleteReplayEvent(controller) {
473
+ const runtimeState = controller.getRuntimeState?.();
474
+ if (runtimeState?.loadingComplete !== true)
475
+ return null;
476
+ const totalRegistered = typeof runtimeState.totalRegistered === "number" &&
477
+ Number.isFinite(runtimeState.totalRegistered)
478
+ ? Math.max(0, runtimeState.totalRegistered)
479
+ : 0;
480
+ const totalLoaded = typeof runtimeState.totalLoaded === "number" &&
481
+ Number.isFinite(runtimeState.totalLoaded)
482
+ ? Math.max(0, runtimeState.totalLoaded)
483
+ : totalRegistered;
484
+ return {
485
+ type: "section-loading-complete",
486
+ totalRegistered,
487
+ totalLoaded,
488
+ currentItemIndex: runtimeState.currentItemIndex,
489
+ timestamp: Date.now(),
490
+ };
491
+ }
492
+ createSectionEventDetachHandler(subscriptionKey, unsubscribeController) {
469
493
  const detach = () => {
470
494
  const current = this.sectionEventSubscriptions.get(subscriptionKey);
471
495
  if (current !== detach) {
@@ -474,7 +498,6 @@ export class ToolkitCoordinator {
474
498
  this.sectionEventSubscriptions.delete(subscriptionKey);
475
499
  unsubscribeController();
476
500
  };
477
- this.sectionEventSubscriptions.set(subscriptionKey, detach);
478
501
  return detach;
479
502
  }
480
503
  /**
@@ -574,67 +597,101 @@ export class ToolkitCoordinator {
574
597
  attemptId: args.attemptId,
575
598
  };
576
599
  const mapKey = this.getSectionControllerMapKey(key);
577
- const existingController = this.sectionControllers.get(mapKey);
578
- if (existingController) {
579
- this.sectionControllerKeys.set(mapKey, key);
580
- if (args.updateExisting !== false) {
581
- // Existing controllers keep their in-memory session state across
582
- // input refresh; updateInput should rebuild composition/runtime view
583
- // without resetting responses.
584
- await existingController.updateInput?.(args.input);
585
- }
600
+ const existingController = await this.resolveExistingSectionController({
601
+ mapKey,
602
+ key,
603
+ input: args.input,
604
+ updateExisting: args.updateExisting,
605
+ });
606
+ if (existingController)
586
607
  return existingController;
587
- }
588
608
  const existingPromise = this.sectionControllerInitPromises.get(mapKey);
589
609
  if (existingPromise)
590
610
  return existingPromise;
591
- const initPromise = (async () => {
592
- const context = {
593
- key,
594
- coordinator: this,
595
- input: args.input,
596
- };
597
- const persistence = await this.resolveSectionPersistence(context);
598
- const defaults = {
599
- createDefaultController: args.createDefaultController,
600
- };
601
- const controller = (await this.hooks.createSectionController?.(context, defaults)) ??
602
- (await defaults.createDefaultController());
603
- await controller.configureSessionPersistence?.({
604
- strategy: persistence,
605
- context,
606
- });
607
- await controller.initialize?.(args.input);
608
- await controller.hydrate?.();
609
- this.sectionControllers.set(mapKey, controller);
610
- this.sectionControllerKeys.set(mapKey, key);
611
- this.emitSectionControllerLifecycle({
612
- type: "ready",
613
- key,
614
- controller,
615
- });
616
- await this.hooks.onSectionControllerReady?.(context, controller);
617
- await this.emitTelemetry("section-controller-ready", {
618
- assessmentId: key.assessmentId,
619
- sectionId: key.sectionId,
620
- attemptId: key.attemptId,
621
- });
622
- return controller;
623
- })().catch((err) => {
624
- this.handleError(err, {
625
- phase: "section-controller-init",
626
- details: {
627
- sectionId: args.sectionId,
628
- attemptId: args.attemptId,
629
- },
630
- });
611
+ const initPromise = this.initializeNewSectionController({
612
+ args,
613
+ key,
614
+ mapKey,
615
+ })
616
+ .catch((err) => {
617
+ this.handleSectionControllerInitError(err, args);
631
618
  throw err;
632
- }).finally(() => {
619
+ })
620
+ .finally(() => {
633
621
  this.sectionControllerInitPromises.delete(mapKey);
634
622
  });
635
623
  this.sectionControllerInitPromises.set(mapKey, initPromise);
636
624
  return initPromise;
637
625
  }
626
+ async resolveExistingSectionController(args) {
627
+ const existingController = this.sectionControllers.get(args.mapKey);
628
+ if (!existingController)
629
+ return undefined;
630
+ this.sectionControllerKeys.set(args.mapKey, args.key);
631
+ if (args.updateExisting !== false) {
632
+ // Existing controllers keep their in-memory session state across
633
+ // input refresh; updateInput should rebuild composition/runtime view
634
+ // without resetting responses.
635
+ await existingController.updateInput?.(args.input);
636
+ }
637
+ return existingController;
638
+ }
639
+ createSectionControllerContext(args) {
640
+ return {
641
+ key: args.key,
642
+ coordinator: this,
643
+ input: args.input,
644
+ };
645
+ }
646
+ async initializeNewSectionController(args) {
647
+ const context = this.createSectionControllerContext({
648
+ key: args.key,
649
+ input: args.args.input,
650
+ });
651
+ const persistence = await this.resolveSectionPersistence(context);
652
+ const defaults = {
653
+ createDefaultController: args.args.createDefaultController,
654
+ };
655
+ const controller = (await this.hooks.createSectionController?.(context, defaults)) ??
656
+ (await defaults.createDefaultController());
657
+ await controller.configureSessionPersistence?.({
658
+ strategy: persistence,
659
+ context,
660
+ });
661
+ await controller.initialize?.(args.args.input);
662
+ await controller.hydrate?.();
663
+ await this.finalizeSectionControllerReady({
664
+ mapKey: args.mapKey,
665
+ key: args.key,
666
+ context,
667
+ controller,
668
+ });
669
+ return controller;
670
+ }
671
+ async finalizeSectionControllerReady(args) {
672
+ this.sectionControllers.set(args.mapKey, args.controller);
673
+ this.sectionControllerKeys.set(args.mapKey, args.key);
674
+ this.emitSectionControllerLifecycle({
675
+ type: "ready",
676
+ key: args.key,
677
+ controller: args.controller,
678
+ });
679
+ await this.hooks.onSectionControllerReady?.(args.context, args.controller);
680
+ await this.emitTelemetry("section-controller-ready", {
681
+ assessmentId: args.key.assessmentId,
682
+ sectionId: args.key.sectionId,
683
+ attemptId: args.key.attemptId,
684
+ });
685
+ }
686
+ handleSectionControllerInitError(err, args) {
687
+ this.handleError(err, {
688
+ phase: "section-controller-init",
689
+ details: {
690
+ sectionId: args.sectionId,
691
+ attemptId: args.attemptId,
692
+ },
693
+ });
694
+ }
638
695
  async disposeSectionController(args) {
639
696
  const key = {
640
697
  assessmentId: this.assessmentId,
@@ -645,26 +702,17 @@ export class ToolkitCoordinator {
645
702
  const controller = this.sectionControllers.get(mapKey);
646
703
  if (!controller)
647
704
  return;
648
- const subscriptionPrefix = `${mapKey}::`;
649
- for (const subscriptionKey of Array.from(this.sectionEventSubscriptions.keys())) {
650
- if (!subscriptionKey.startsWith(subscriptionPrefix))
651
- continue;
652
- this.sectionEventSubscriptions.get(subscriptionKey)?.();
653
- }
654
- const context = {
705
+ this.detachSectionEventSubscriptionsForMapKey(mapKey);
706
+ const context = this.createSectionControllerContext({
655
707
  key,
656
- coordinator: this,
657
- };
708
+ input: undefined,
709
+ });
658
710
  try {
659
- if (args.persistBeforeDispose !== false) {
660
- await controller.persist?.();
661
- }
662
- await controller.dispose?.();
663
- await this.hooks.onSectionControllerDispose?.(context, controller);
664
- await this.emitTelemetry("section-controller-disposed", {
665
- assessmentId: key.assessmentId,
666
- sectionId: key.sectionId,
667
- attemptId: key.attemptId,
711
+ await this.runSectionControllerDisposePipeline({
712
+ key,
713
+ context,
714
+ controller,
715
+ persistBeforeDispose: args.persistBeforeDispose,
668
716
  });
669
717
  }
670
718
  catch (err) {
@@ -677,19 +725,47 @@ export class ToolkitCoordinator {
677
725
  });
678
726
  }
679
727
  finally {
680
- this.sectionControllers.delete(mapKey);
681
- this.sectionControllerKeys.delete(mapKey);
682
- this.emitSectionControllerLifecycle({
683
- type: "disposed",
728
+ await this.finalizeSectionControllerDispose({
729
+ mapKey,
684
730
  key,
731
+ context,
732
+ clearPersistence: args.clearPersistence,
685
733
  });
686
- if (args.clearPersistence) {
687
- const strategy = this.sectionPersistenceStrategies.get(mapKey);
688
- await strategy?.clearSession?.(context);
689
- }
690
- this.sectionPersistenceStrategies.delete(mapKey);
691
734
  }
692
735
  }
736
+ detachSectionEventSubscriptionsForMapKey(mapKey) {
737
+ const subscriptionPrefix = `${mapKey}::`;
738
+ for (const subscriptionKey of Array.from(this.sectionEventSubscriptions.keys())) {
739
+ if (!subscriptionKey.startsWith(subscriptionPrefix))
740
+ continue;
741
+ this.sectionEventSubscriptions.get(subscriptionKey)?.();
742
+ }
743
+ }
744
+ async runSectionControllerDisposePipeline(args) {
745
+ if (args.persistBeforeDispose !== false) {
746
+ await args.controller.persist?.();
747
+ }
748
+ await args.controller.dispose?.();
749
+ await this.hooks.onSectionControllerDispose?.(args.context, args.controller);
750
+ await this.emitTelemetry("section-controller-disposed", {
751
+ assessmentId: args.key.assessmentId,
752
+ sectionId: args.key.sectionId,
753
+ attemptId: args.key.attemptId,
754
+ });
755
+ }
756
+ async finalizeSectionControllerDispose(args) {
757
+ this.sectionControllers.delete(args.mapKey);
758
+ this.sectionControllerKeys.delete(args.mapKey);
759
+ this.emitSectionControllerLifecycle({
760
+ type: "disposed",
761
+ key: args.key,
762
+ });
763
+ if (args.clearPersistence) {
764
+ const strategy = this.sectionPersistenceStrategies.get(args.mapKey);
765
+ await strategy?.clearSession?.(args.context);
766
+ }
767
+ this.sectionPersistenceStrategies.delete(args.mapKey);
768
+ }
693
769
  /**
694
770
  * Initialize TTS service with provider
695
771
  */
@@ -706,92 +782,24 @@ export class ToolkitCoordinator {
706
782
  async _initializeTTS(config) {
707
783
  if (this.ttsInitialized)
708
784
  return;
785
+ const resolvedToolConfig = this.resolveTTSToolConfig(config);
786
+ const resolvedBackend = resolveTTSBackend(resolvedToolConfig);
787
+ const runtimeTTSConfig = buildRuntimeTTSConfig(resolvedToolConfig);
709
788
  await this.hooks.onBeforeTTSInit?.({
710
789
  phase: "tts-init",
711
790
  details: {
712
- backend: config?.backend,
791
+ backend: resolvedBackend,
713
792
  },
714
793
  });
715
794
  await this.emitTelemetry("tts-init-start", {
716
- backend: config?.backend ??
717
- this.config.tools?.providers?.tts
718
- ?.backend ??
719
- "browser",
795
+ backend: resolvedBackend,
720
796
  });
721
- const resolvedToolConfig = config ||
722
- (this.config.tools?.providers?.tts || {});
723
- const resolvedBackend = resolvedToolConfig.backend || "browser";
724
- const runtimeProvider = resolvedToolConfig.serverProvider ||
725
- resolvedToolConfig.provider ||
726
- (resolvedBackend === "polly" || resolvedBackend === "google"
727
- ? resolvedBackend
728
- : undefined);
729
- const transportMode = resolvedToolConfig.transportMode ||
730
- (runtimeProvider === "custom"
731
- ? "custom"
732
- : "pie");
733
- const runtimeTTSConfig = {
734
- voice: resolvedToolConfig.defaultVoice,
735
- rate: resolvedToolConfig.rate,
736
- pitch: resolvedToolConfig.pitch,
737
- providerOptions: {
738
- ...(resolvedToolConfig.language
739
- ? {
740
- locale: resolvedToolConfig.language,
741
- }
742
- : {}),
743
- ...(resolvedBackend === "polly" && resolvedToolConfig.engine
744
- ? {
745
- engine: resolvedToolConfig.engine,
746
- }
747
- : {}),
748
- ...(resolvedBackend === "polly" &&
749
- typeof resolvedToolConfig.sampleRate === "number"
750
- ? {
751
- sampleRate: resolvedToolConfig.sampleRate,
752
- }
753
- : {}),
754
- ...(resolvedBackend === "polly" && resolvedToolConfig.format
755
- ? {
756
- format: resolvedToolConfig.format,
757
- }
758
- : {}),
759
- ...(resolvedBackend === "polly"
760
- ? {
761
- speechMarkTypes: resolvedToolConfig.speechMarksMode === "word+sentence"
762
- ? ["word", "sentence"]
763
- : ["word"],
764
- }
765
- : {}),
766
- ...(transportMode === "custom" &&
767
- typeof resolvedToolConfig.cache === "boolean"
768
- ? { cache: resolvedToolConfig.cache }
769
- : {}),
770
- ...(transportMode === "custom" && resolvedToolConfig.speedRate
771
- ? { speedRate: resolvedToolConfig.speedRate }
772
- : {}),
773
- ...(transportMode === "custom" && resolvedToolConfig.lang_id
774
- ? { lang_id: resolvedToolConfig.lang_id }
775
- : {}),
776
- },
777
- apiEndpoint: resolvedToolConfig.apiEndpoint,
778
- provider: runtimeProvider,
779
- language: resolvedToolConfig.language,
780
- transportMode,
781
- endpointMode: resolvedToolConfig.endpointMode,
782
- endpointValidationMode: resolvedToolConfig.endpointValidationMode,
783
- includeAuthOnAssetFetch: resolvedToolConfig.includeAuthOnAssetFetch,
784
- validateEndpoint: resolvedToolConfig.validateEndpoint,
785
- };
786
797
  // Try to use TTS provider from registry if available
787
798
  if (this.toolProviderRegistry.has("tts")) {
788
799
  try {
789
800
  const ttsProvider = await this.ensureProviderReady("tts");
790
801
  const providerInstance = await ttsProvider.createInstance();
791
- await this.ttsService.initialize(providerInstance, runtimeTTSConfig);
792
- this.ttsService.setCatalogResolver(this.catalogResolver);
793
- this.ttsInitialized = true;
794
- await this.hooks.onTTSReady?.();
802
+ await this.initializeTTSService(providerInstance, runtimeTTSConfig);
795
803
  await this.emitTelemetry("tts-init-success", {
796
804
  provider: "registry",
797
805
  });
@@ -805,10 +813,7 @@ export class ToolkitCoordinator {
805
813
  // Fallback to browser provider
806
814
  const provider = new BrowserTTSProvider();
807
815
  try {
808
- await this.ttsService.initialize(provider, runtimeTTSConfig);
809
- this.ttsService.setCatalogResolver(this.catalogResolver);
810
- this.ttsInitialized = true;
811
- await this.hooks.onTTSReady?.();
816
+ await this.initializeTTSService(provider, runtimeTTSConfig);
812
817
  await this.emitTelemetry("tts-init-success", {
813
818
  provider: "browser-fallback",
814
819
  });
@@ -823,6 +828,16 @@ export class ToolkitCoordinator {
823
828
  throw normalized;
824
829
  }
825
830
  }
831
+ resolveTTSToolConfig(config) {
832
+ return (config ||
833
+ (this.config.tools?.providers?.tts || {}));
834
+ }
835
+ async initializeTTSService(provider, config) {
836
+ await this.ttsService.initialize(provider, config);
837
+ this.ttsService.setCatalogResolver(this.catalogResolver);
838
+ this.ttsInitialized = true;
839
+ await this.hooks.onTTSReady?.();
840
+ }
826
841
  /**
827
842
  * Get all services as a bundle for section player convenience.
828
843
  * Section player can extract and pass services to child components.