@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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(
|
|
433
|
+
this.sectionEventListenerIds.set(listener, listenerId);
|
|
410
434
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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 = (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
649
|
-
|
|
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
|
-
|
|
657
|
-
};
|
|
708
|
+
input: undefined,
|
|
709
|
+
});
|
|
658
710
|
try {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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.
|
|
681
|
-
|
|
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:
|
|
791
|
+
backend: resolvedBackend,
|
|
713
792
|
},
|
|
714
793
|
});
|
|
715
794
|
await this.emitTelemetry("tts-init-start", {
|
|
716
|
-
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.
|
|
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.
|
|
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.
|