@pie-players/pie-assessment-toolkit 1.0.0-beta.7 → 1.0.0-beta.8
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/custom-elements.json +7 -0
- package/dist/components/ItemScope.custom-element.js +1 -1
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +7 -7
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-bsww9e8f.js +4 -0
- package/dist/components/chunks/{ItemToolBar-pxan157g.js → ItemToolBar-gsb96731.js} +2 -2
- package/dist/components/chunks/ItemToolBar-na4e5vwt.js +2 -0
- package/dist/components/chunks/{ItemToolBar-xwjrpdmz.js → ItemToolBar-wcw4yxjy.js} +1 -1
- package/dist/components/chunks/ItemToolBar-x998kkvy.js +2 -0
- package/dist/components/chunks/ItemToolBar-zvy0hskj.js +40 -0
- package/dist/components/chunks/sre-engine-wazxma67.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/runtime/SectionControllerBinding.d.ts +11 -0
- package/dist/runtime/SectionControllerBinding.js +8 -0
- package/dist/runtime/registration-events.d.ts +28 -1
- package/dist/runtime/registration-events.js +19 -0
- package/dist/runtime/shell-event-bridge.d.ts +7 -1
- package/dist/runtime/shell-event-bridge.js +44 -1
- package/dist/runtime/shell-scope.d.ts +2 -2
- package/dist/runtime/shell-scope.js +14 -5
- package/dist/services/SSMLExtractor.js +8 -23
- package/dist/services/ToolRegistry.d.ts +11 -3
- package/dist/services/ToolRegistry.js +8 -4
- package/dist/services/ToolkitCoordinator.d.ts +12 -47
- package/dist/services/ToolkitCoordinator.js +45 -284
- package/dist/services/section-controller-types.d.ts +40 -0
- package/dist/services/tool-config-validation.d.ts +1 -1
- package/dist/services/tool-context.js +94 -11
- package/dist/services/tool-icons.js +2 -0
- package/dist/services/tool-provider-defaults.d.ts +13 -0
- package/dist/services/tool-provider-defaults.js +20 -0
- package/dist/services/tts/browser-provider.js +3 -1
- package/dist/services/tts/tts-lifecycle.d.ts +142 -0
- package/dist/services/tts/tts-lifecycle.js +341 -0
- package/dist/tools/content-surfaces.d.ts +35 -0
- package/dist/tools/content-surfaces.js +35 -0
- package/dist/tools/registration.d.ts +2 -0
- package/dist/tools/registration.js +5 -0
- package/package.json +8 -8
- package/dist/components/chunks/ItemToolBar-4vq9nbxz.js +0 -2
- package/dist/components/chunks/ItemToolBar-f971t0cw.js +0 -4
- package/dist/components/chunks/ItemToolBar-n05xjkp8.js +0 -2
- package/dist/components/chunks/ItemToolBar-rgmvw9sm.js +0 -40
- package/dist/components/chunks/sre-engine-dq0w45fv.js +0 -1
|
@@ -25,6 +25,8 @@ const TOOL_FALLBACK_ICONS = {
|
|
|
25
25
|
"magnifying-glass": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16a6.47 6.47 0 0 0 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5Zm-6 0A4.5 4.5 0 1 1 14 9.5 4.49 4.49 0 0 1 9.5 14ZM12 10h-2v2H9v-2H7V9h2V7h1v2h2v1Z" fill="currentColor"/></svg>',
|
|
26
26
|
ruler: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m8.8 10.95 2.15-2.175-1.4-1.425-1.1 1.1-1.4-1.4 1.075-1.1L7 4.825 4.825 7 8.8 10.95Zm8.2 8.225L19.175 17l-1.125-1.125-1.1 1.075-1.4-1.4 1.075-1.1-1.425-1.4-2.15 2.15L17 19.175ZM7.25 21H3v-4.25l4.375-4.375L2 7l5-5 5.4 5.4 3.775-3.8c.2-.2.425-.35.675-.45a2.068 2.068 0 0 1 1.55 0c.25.1.475.25.675.45L20.4 4.95c.2.2.35.425.45.675.1.25.15.508.15.775a1.975 1.975 0 0 1-.6 1.425l-3.775 3.8L22 17l-5 5-5.375-5.375L7.25 21ZM5 19h1.4l9.8-9.775L14.775 7.8 5 17.6V19Z" fill="currentColor"/></svg>',
|
|
27
27
|
"book-open": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 6.25a6.6 6.6 0 0 0-2.3-1.32A8.6 8.6 0 0 0 7 4.5c-.63 0-1.25.06-1.85.18-.6.12-1.19.3-1.76.54a1.5 1.5 0 0 0-.93 1.4v10.1c0 .52.26.98.68 1.24.42.26.92.28 1.35.06.36-.18.75-.31 1.18-.4.42-.09.86-.13 1.33-.13.78 0 1.53.14 2.24.42.7.28 1.35.7 1.93 1.24l.83-.75.83.75c.58-.54 1.22-.96 1.93-1.24a6.1 6.1 0 0 1 2.24-.42c.47 0 .91.04 1.33.13.43.09.82.22 1.18.4.43.22.93.2 1.35-.06.42-.26.68-.72.68-1.24V6.62c0-.6-.36-1.15-.93-1.4a9.3 9.3 0 0 0-1.76-.54A9.8 9.8 0 0 0 17 4.5a8.6 8.6 0 0 0-2.7.43A6.6 6.6 0 0 0 12 6.25Zm1 10.06V8.02c.55-.5 1.16-.88 1.84-1.13A6 6 0 0 1 17 6.5c.45 0 .89.04 1.32.11.43.08.85.19 1.26.34v9.24a8.2 8.2 0 0 0-1.28-.24A10 10 0 0 0 17 15.85c-.72 0-1.42.09-2.09.26a7.7 7.7 0 0 0-1.91.8Zm-2 0a7.7 7.7 0 0 0-1.91-.8 8.4 8.4 0 0 0-2.09-.26c-.44 0-.87.02-1.3.07-.43.05-.86.13-1.28.24V6.95c.41-.15.83-.26 1.26-.34A7.7 7.7 0 0 1 7 6.5c.76 0 1.48.13 2.16.39.68.25 1.29.63 1.84 1.13Z" fill="currentColor"/></svg>',
|
|
28
|
+
highlighter: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22 24H2v-4h20v4ZM13.06 5.19l3.75 3.75L7.75 18H4v-3.75l9.06-9.06Zm4.82 2.68-3.75-3.75 1.83-1.83a1 1 0 0 1 1.41 0l2.34 2.34a1 1 0 0 1 0 1.41l-1.83 1.83Z" fill="currentColor"/></svg>',
|
|
29
|
+
strikethrough: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Zm-3.4 14L12 13.4 8.4 17 7 15.6l3.6-3.6L7 8.4 8.4 7l3.6 3.6L15.6 7 17 8.4 13.4 12l3.6 3.6-1.4 1.4Z" fill="currentColor"/></svg>',
|
|
28
30
|
photo: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 21c-.55 0-1.02-.2-1.41-.59A1.93 1.93 0 0 1 2 19V5c0-.55.2-1.02.59-1.41A1.93 1.93 0 0 1 4 3h16c.55 0 1.02.2 1.41.59.39.39.59.86.59 1.41v14c0 .55-.2 1.02-.59 1.41-.39.39-.86.59-1.41.59H4Zm0-2h16V5H4v14Zm1.5-2.5h13l-4.1-5.47-3.4 4.47-2.5-3.25L5.5 16.5ZM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" fill="currentColor"/></svg>',
|
|
29
31
|
};
|
|
30
32
|
/** Icon markup for `name`, or `null` when nothing built in matches. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider configs a coordinator applies when the host's tools config
|
|
3
|
+
* names none for a capability.
|
|
4
|
+
*
|
|
5
|
+
* This module names capabilities on purpose, like `services/support-conflicts.ts`:
|
|
6
|
+
* a default is a statement about which capabilities a deployment starts with,
|
|
7
|
+
* which is not a decision the capability-neutral coordination layer gets to
|
|
8
|
+
* make. The coordinator applies the table without naming its entries —
|
|
9
|
+
* `check-capability-neutrality` holds it to that. These are the coordinator's
|
|
10
|
+
* previous inline defaults, moved verbatim.
|
|
11
|
+
*/
|
|
12
|
+
import type { ToolProvidersConfig } from "./tools-config-normalizer.js";
|
|
13
|
+
export declare const DEFAULT_TOOL_PROVIDERS: ToolProvidersConfig;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider configs a coordinator applies when the host's tools config
|
|
3
|
+
* names none for a capability.
|
|
4
|
+
*
|
|
5
|
+
* This module names capabilities on purpose, like `services/support-conflicts.ts`:
|
|
6
|
+
* a default is a statement about which capabilities a deployment starts with,
|
|
7
|
+
* which is not a decision the capability-neutral coordination layer gets to
|
|
8
|
+
* make. The coordinator applies the table without naming its entries —
|
|
9
|
+
* `check-capability-neutrality` holds it to that. These are the coordinator's
|
|
10
|
+
* previous inline defaults, moved verbatim.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_TOOL_PROVIDERS = {
|
|
13
|
+
textToSpeech: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
backend: "browser",
|
|
16
|
+
},
|
|
17
|
+
annotationToolbar: {
|
|
18
|
+
enabled: true,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Text-to-speech provider using the browser's Web Speech API.
|
|
5
5
|
* Works in all modern browsers that support SpeechSynthesis.
|
|
6
6
|
*/
|
|
7
|
+
import { languageTagWithoutVariants } from "@pie-players/pie-players-shared/i18n/language-tags";
|
|
7
8
|
import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
|
|
8
9
|
import { extractSpokenText, isSsmlDocument } from "./ssml/spoken-text.js";
|
|
9
10
|
import { segmentSentences as segmentTextToSentences } from "./text-segmentation.js";
|
|
@@ -383,10 +384,11 @@ class BrowserTTSProviderImpl {
|
|
|
383
384
|
* The language of the content being read, which the toolkit sets per speak as
|
|
384
385
|
* `providerOptions.contentLanguage` when the content or the host names one.
|
|
385
386
|
*/
|
|
387
|
+
/** The content language without variant or private-use subtags, which voices lack. */
|
|
386
388
|
getContentLocale() {
|
|
387
389
|
const language = this.config?.providerOptions?.contentLanguage;
|
|
388
390
|
return typeof language === "string" && language.trim()
|
|
389
|
-
? language
|
|
391
|
+
? languageTagWithoutVariants(language)
|
|
390
392
|
: undefined;
|
|
391
393
|
}
|
|
392
394
|
getSegmentationPolicy() {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text-to-speech lifecycle of a `ToolkitCoordinator`: initialization from
|
|
3
|
+
* the tool config, readiness, reconfiguration, and the missing-provider report.
|
|
4
|
+
*
|
|
5
|
+
* This module is the one place in the toolkit's coordination layer allowed to
|
|
6
|
+
* name the read-aloud capability, the same way `services/tts/**` already is:
|
|
7
|
+
* the coordinator stays capability-agnostic — it hands this lifecycle the
|
|
8
|
+
* services it needs through `TtsLifecycleHost` and answers only generic
|
|
9
|
+
* questions (`ownsTool`, `isStarted`) about it — and
|
|
10
|
+
* `check-capability-neutrality` holds the coordinator to that, with one
|
|
11
|
+
* documented allowance for its public `textToSpeech`-typed config accessors.
|
|
12
|
+
*
|
|
13
|
+
* Behavior is moved, not redesigned: each method here was a private
|
|
14
|
+
* `ToolkitCoordinator` member, and the host adapters bind the same calls the
|
|
15
|
+
* members made.
|
|
16
|
+
*/
|
|
17
|
+
import type { AccessibilityCatalogResolver } from "../AccessibilityCatalogResolver.js";
|
|
18
|
+
import type { ToolProviderConfig, ToolProvidersConfig } from "../tools-config-normalizer.js";
|
|
19
|
+
import type { TTSService } from "../TTSService.js";
|
|
20
|
+
import type { ToolProviderApi } from "../tool-providers/ToolProviderApi.js";
|
|
21
|
+
import type { ToolRegistration } from "../ToolRegistry.js";
|
|
22
|
+
/** The tool id the read-aloud capability registers under. */
|
|
23
|
+
export declare const TTS_TOOL_ID = "textToSpeech";
|
|
24
|
+
/**
|
|
25
|
+
* The coordinator services the TTS lifecycle runs against. The coordinator
|
|
26
|
+
* binds these once, to its own members, so its privates stay private and the
|
|
27
|
+
* seam between the generic coordinator and the capability stays explicit.
|
|
28
|
+
*/
|
|
29
|
+
export interface TtsLifecycleHost {
|
|
30
|
+
/** Throws `ToolkitCoordinatorDisposedError` once the coordinator is disposed. */
|
|
31
|
+
assertNotDisposed(): void;
|
|
32
|
+
/** Whether disposal has started; in-flight work stops instead of finishing. */
|
|
33
|
+
isDisposalStarted(): boolean;
|
|
34
|
+
/** Whether the error is the coordinator's own disposed signal, to rethrow. */
|
|
35
|
+
isDisposalError(error: unknown): boolean;
|
|
36
|
+
emitTelemetry(eventName: string, payload?: Record<string, unknown>): Promise<void>;
|
|
37
|
+
hooks: {
|
|
38
|
+
onBeforeTTSInit?: ((context: {
|
|
39
|
+
phase: "tts-init";
|
|
40
|
+
details: {
|
|
41
|
+
backend: string;
|
|
42
|
+
};
|
|
43
|
+
}) => void | Promise<void>) | undefined;
|
|
44
|
+
onTTSReady?: (() => void | Promise<void>) | undefined;
|
|
45
|
+
};
|
|
46
|
+
toolProviderRegistry: {
|
|
47
|
+
has(toolId: string): boolean;
|
|
48
|
+
unregister(toolId: string): Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
initializeProvider(toolId: string, fallbackFollows: boolean): Promise<ToolProviderApi>;
|
|
51
|
+
ttsService: TTSService;
|
|
52
|
+
catalogResolver: AccessibilityCatalogResolver;
|
|
53
|
+
clearDegradedTool(toolId: string): void;
|
|
54
|
+
reportStartFailure(error: unknown, context: {
|
|
55
|
+
phase: "tts-init";
|
|
56
|
+
}, toolIds: Iterable<string>): void;
|
|
57
|
+
isToolGranted(toolId: string): boolean;
|
|
58
|
+
/** Whether provider initialization waits for a policy grant. */
|
|
59
|
+
lazyInit: boolean;
|
|
60
|
+
/** Whether the tool registry may still be replaced by a bound toolkit's. */
|
|
61
|
+
isToolRegistryPending(): boolean;
|
|
62
|
+
/** The validated `tools.providers` config, defaults applied. */
|
|
63
|
+
providersConfig(): ToolProvidersConfig | undefined;
|
|
64
|
+
getProviderDescriptorTools(): ToolRegistration[];
|
|
65
|
+
registerProviderFromTool(tool: ToolRegistration, replace?: boolean): Promise<void>;
|
|
66
|
+
notifyReadyChange(): void;
|
|
67
|
+
}
|
|
68
|
+
/** The read-aloud-specific merge in a generic tool-config update. */
|
|
69
|
+
export declare function deepMergeMathSpeech(toolId: string, current: Partial<ToolProviderConfig>, updates: Partial<ToolProviderConfig>): Record<string, unknown> | undefined;
|
|
70
|
+
export declare class TtsLifecycle {
|
|
71
|
+
private readonly host;
|
|
72
|
+
private initialized;
|
|
73
|
+
/** Text-to-speech failed to start and policy does not grant it. */
|
|
74
|
+
private degraded;
|
|
75
|
+
private initPromise?;
|
|
76
|
+
private reconfigurePromise?;
|
|
77
|
+
/**
|
|
78
|
+
* Whether {@link TtsLifecycle.initializeIfNeeded} has already reported a
|
|
79
|
+
* non-browser backend falling back to browser speech because no
|
|
80
|
+
* text-to-speech provider is registered. Once per coordinator: a
|
|
81
|
+
* text-to-speech config change re-runs initialization, and the missing
|
|
82
|
+
* provider is the same gap each time.
|
|
83
|
+
*/
|
|
84
|
+
private reportedMissingProvider;
|
|
85
|
+
/**
|
|
86
|
+
* The backend of a fallback missing-provider report held back while the
|
|
87
|
+
* registry was unsettled, reported if the bound toolkit has no registry.
|
|
88
|
+
*/
|
|
89
|
+
private heldMissingProviderBackend;
|
|
90
|
+
constructor(host: TtsLifecycleHost);
|
|
91
|
+
get started(): boolean;
|
|
92
|
+
get failedUngranted(): boolean;
|
|
93
|
+
/** Whether `toolId` is the tool this lifecycle brings up. */
|
|
94
|
+
ownsTool(toolId: string): boolean;
|
|
95
|
+
/** Whether initialization or a reconfiguration is in flight. */
|
|
96
|
+
isStarted(): boolean;
|
|
97
|
+
/** The in-flight initialization and reconfiguration, for disposal flushes. */
|
|
98
|
+
flushInitPromises(pending: Set<Promise<unknown>>): void;
|
|
99
|
+
/**
|
|
100
|
+
* The runtime `mathSpeech` provider options for the current tool config, as
|
|
101
|
+
* the TTS service's math-speech source reads them.
|
|
102
|
+
*/
|
|
103
|
+
mathSpeechRuntimeOptions(): import("./math-speech.js").SREMathSpeechOptions | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Initialize the TTS service from the `textToSpeech` tool config.
|
|
106
|
+
*/
|
|
107
|
+
ensureReady(): Promise<void>;
|
|
108
|
+
private waitForPendingReconfigure;
|
|
109
|
+
private initializeIfNeeded;
|
|
110
|
+
private resolveToolConfig;
|
|
111
|
+
private initializeService;
|
|
112
|
+
private reportMissingProvider;
|
|
113
|
+
/**
|
|
114
|
+
* Report a missing-provider report held back while the registry was
|
|
115
|
+
* unsettled, now that the registry has settled without one.
|
|
116
|
+
*/
|
|
117
|
+
reportHeldMissingProvider(): void;
|
|
118
|
+
/**
|
|
119
|
+
* Drop a held missing-provider report without reporting it: a real registry
|
|
120
|
+
* settled, so the stale gap never applied.
|
|
121
|
+
*/
|
|
122
|
+
discardHeldMissingProvider(): void;
|
|
123
|
+
/**
|
|
124
|
+
* Readiness waits for text-to-speech when it is enabled and either starts
|
|
125
|
+
* eagerly or is granted: a granted read-aloud fails before the learner starts.
|
|
126
|
+
*/
|
|
127
|
+
requiredForReadiness(): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Starts text-to-speech for readiness. A failure policy does not grant leaves
|
|
130
|
+
* the tool degraded, reporting itself unavailable, and readiness settles; a
|
|
131
|
+
* granted tool's failure rejects.
|
|
132
|
+
*/
|
|
133
|
+
startForReadiness(): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Re-register the text-to-speech provider for the current config and registry,
|
|
136
|
+
* then re-initialize unless initialization is lazy. An initialization already
|
|
137
|
+
* running finishes first: it would otherwise mark its provider ready after the
|
|
138
|
+
* reset.
|
|
139
|
+
*/
|
|
140
|
+
scheduleReconfigure(): void;
|
|
141
|
+
private reconfigure;
|
|
142
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text-to-speech lifecycle of a `ToolkitCoordinator`: initialization from
|
|
3
|
+
* the tool config, readiness, reconfiguration, and the missing-provider report.
|
|
4
|
+
*
|
|
5
|
+
* This module is the one place in the toolkit's coordination layer allowed to
|
|
6
|
+
* name the read-aloud capability, the same way `services/tts/**` already is:
|
|
7
|
+
* the coordinator stays capability-agnostic — it hands this lifecycle the
|
|
8
|
+
* services it needs through `TtsLifecycleHost` and answers only generic
|
|
9
|
+
* questions (`ownsTool`, `isStarted`) about it — and
|
|
10
|
+
* `check-capability-neutrality` holds the coordinator to that, with one
|
|
11
|
+
* documented allowance for its public `textToSpeech`-typed config accessors.
|
|
12
|
+
*
|
|
13
|
+
* Behavior is moved, not redesigned: each method here was a private
|
|
14
|
+
* `ToolkitCoordinator` member, and the host adapters bind the same calls the
|
|
15
|
+
* members made.
|
|
16
|
+
*/
|
|
17
|
+
import { BrowserTTSProvider, browserFallbackConfig, } from "./browser-provider.js";
|
|
18
|
+
import { buildRuntimeTTSConfig, resolveTTSBackend, resolveTTSRuntimeSettings, } from "../tts-runtime-config.js";
|
|
19
|
+
/** The tool id the read-aloud capability registers under. */
|
|
20
|
+
export const TTS_TOOL_ID = "textToSpeech";
|
|
21
|
+
/** The read-aloud-specific merge in a generic tool-config update. */
|
|
22
|
+
export function deepMergeMathSpeech(toolId, current, updates) {
|
|
23
|
+
if (toolId !== TTS_TOOL_ID ||
|
|
24
|
+
!isPlainRecord(current.mathSpeech) ||
|
|
25
|
+
!isPlainRecord(updates.mathSpeech)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return { ...current.mathSpeech, ...updates.mathSpeech };
|
|
29
|
+
}
|
|
30
|
+
function isPlainRecord(value) {
|
|
31
|
+
return (typeof value === "object" && value !== null && !Array.isArray(value));
|
|
32
|
+
}
|
|
33
|
+
export class TtsLifecycle {
|
|
34
|
+
host;
|
|
35
|
+
initialized = false;
|
|
36
|
+
/** Text-to-speech failed to start and policy does not grant it. */
|
|
37
|
+
degraded = false;
|
|
38
|
+
initPromise;
|
|
39
|
+
reconfigurePromise;
|
|
40
|
+
/**
|
|
41
|
+
* Whether {@link TtsLifecycle.initializeIfNeeded} has already reported a
|
|
42
|
+
* non-browser backend falling back to browser speech because no
|
|
43
|
+
* text-to-speech provider is registered. Once per coordinator: a
|
|
44
|
+
* text-to-speech config change re-runs initialization, and the missing
|
|
45
|
+
* provider is the same gap each time.
|
|
46
|
+
*/
|
|
47
|
+
reportedMissingProvider = false;
|
|
48
|
+
/**
|
|
49
|
+
* The backend of a fallback missing-provider report held back while the
|
|
50
|
+
* registry was unsettled, reported if the bound toolkit has no registry.
|
|
51
|
+
*/
|
|
52
|
+
heldMissingProviderBackend = null;
|
|
53
|
+
constructor(host) {
|
|
54
|
+
this.host = host;
|
|
55
|
+
}
|
|
56
|
+
get started() {
|
|
57
|
+
return this.initialized;
|
|
58
|
+
}
|
|
59
|
+
get failedUngranted() {
|
|
60
|
+
return this.degraded;
|
|
61
|
+
}
|
|
62
|
+
/** Whether `toolId` is the tool this lifecycle brings up. */
|
|
63
|
+
ownsTool(toolId) {
|
|
64
|
+
return toolId === TTS_TOOL_ID;
|
|
65
|
+
}
|
|
66
|
+
/** Whether initialization or a reconfiguration is in flight. */
|
|
67
|
+
isStarted() {
|
|
68
|
+
return (this.initialized ||
|
|
69
|
+
this.initPromise !== undefined ||
|
|
70
|
+
this.reconfigurePromise !== undefined);
|
|
71
|
+
}
|
|
72
|
+
/** The in-flight initialization and reconfiguration, for disposal flushes. */
|
|
73
|
+
flushInitPromises(pending) {
|
|
74
|
+
if (this.initPromise)
|
|
75
|
+
pending.add(this.initPromise);
|
|
76
|
+
if (this.reconfigurePromise)
|
|
77
|
+
pending.add(this.reconfigurePromise);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The runtime `mathSpeech` provider options for the current tool config, as
|
|
81
|
+
* the TTS service's math-speech source reads them.
|
|
82
|
+
*/
|
|
83
|
+
mathSpeechRuntimeOptions() {
|
|
84
|
+
return buildRuntimeTTSConfig(resolveTTSRuntimeSettings(this.resolveToolConfig())).providerOptions?.mathSpeech;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Initialize the TTS service from the `textToSpeech` tool config.
|
|
88
|
+
*/
|
|
89
|
+
async ensureReady() {
|
|
90
|
+
this.host.assertNotDisposed();
|
|
91
|
+
await this.waitForPendingReconfigure();
|
|
92
|
+
this.host.assertNotDisposed();
|
|
93
|
+
if (this.initialized)
|
|
94
|
+
return;
|
|
95
|
+
if (this.initPromise)
|
|
96
|
+
return this.initPromise;
|
|
97
|
+
this.initPromise = this.initializeIfNeeded()
|
|
98
|
+
.then(() => this.host.assertNotDisposed())
|
|
99
|
+
.finally(() => {
|
|
100
|
+
this.initPromise = undefined;
|
|
101
|
+
this.host.notifyReadyChange();
|
|
102
|
+
});
|
|
103
|
+
return this.initPromise;
|
|
104
|
+
}
|
|
105
|
+
waitForPendingReconfigure() {
|
|
106
|
+
let pending = this.reconfigurePromise;
|
|
107
|
+
const wait = async () => {
|
|
108
|
+
while (pending) {
|
|
109
|
+
await pending;
|
|
110
|
+
pending = this.reconfigurePromise;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
return wait();
|
|
114
|
+
}
|
|
115
|
+
async initializeIfNeeded() {
|
|
116
|
+
if (this.initialized)
|
|
117
|
+
return;
|
|
118
|
+
const resolvedToolConfig = this.resolveToolConfig();
|
|
119
|
+
const runtimeSettings = resolveTTSRuntimeSettings(resolvedToolConfig);
|
|
120
|
+
const resolvedBackend = resolveTTSBackend(runtimeSettings);
|
|
121
|
+
const runtimeTTSConfig = buildRuntimeTTSConfig(runtimeSettings);
|
|
122
|
+
await this.host.hooks.onBeforeTTSInit?.({
|
|
123
|
+
phase: "tts-init",
|
|
124
|
+
details: {
|
|
125
|
+
backend: resolvedBackend,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
this.host.assertNotDisposed();
|
|
129
|
+
await this.host.emitTelemetry("pie-toolkit-tts-init-start", {
|
|
130
|
+
backend: resolvedBackend,
|
|
131
|
+
});
|
|
132
|
+
await this.host.emitTelemetry("pie-tool-init-start", {
|
|
133
|
+
toolId: TTS_TOOL_ID,
|
|
134
|
+
operation: "tts-init",
|
|
135
|
+
backend: resolvedBackend,
|
|
136
|
+
});
|
|
137
|
+
if (this.host.toolProviderRegistry.has(TTS_TOOL_ID)) {
|
|
138
|
+
try {
|
|
139
|
+
// Browser speech follows a server provider that fails to start.
|
|
140
|
+
const ttsProvider = await this.host.initializeProvider(TTS_TOOL_ID, true);
|
|
141
|
+
const providerInstance = await ttsProvider.createInstance();
|
|
142
|
+
await this.initializeService(providerInstance, runtimeTTSConfig);
|
|
143
|
+
await this.host.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
144
|
+
provider: "registry",
|
|
145
|
+
});
|
|
146
|
+
await this.host.emitTelemetry("pie-tool-init-success", {
|
|
147
|
+
toolId: TTS_TOOL_ID,
|
|
148
|
+
operation: "tts-init",
|
|
149
|
+
backend: resolvedBackend,
|
|
150
|
+
provider: "registry",
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (this.host.isDisposalError(error))
|
|
156
|
+
throw error;
|
|
157
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
158
|
+
await this.host.emitTelemetry("pie-tool-init-fallback", {
|
|
159
|
+
toolId: TTS_TOOL_ID,
|
|
160
|
+
operation: "tts-init",
|
|
161
|
+
backend: resolvedBackend,
|
|
162
|
+
fromProvider: "registry",
|
|
163
|
+
toProvider: "browser",
|
|
164
|
+
reason: normalized.message,
|
|
165
|
+
});
|
|
166
|
+
console.warn("[ToolkitCoordinator] Failed to initialize TTS via registry, falling back to browser provider:", normalized);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (resolvedBackend !== "browser") {
|
|
170
|
+
this.reportMissingProvider(resolvedBackend);
|
|
171
|
+
}
|
|
172
|
+
// Browser speech, configured or standing in for a backend that has no
|
|
173
|
+
// provider or failed to start.
|
|
174
|
+
const provider = new BrowserTTSProvider();
|
|
175
|
+
try {
|
|
176
|
+
await this.initializeService(provider, resolvedBackend === "browser"
|
|
177
|
+
? runtimeTTSConfig
|
|
178
|
+
: browserFallbackConfig(runtimeTTSConfig));
|
|
179
|
+
await this.host.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
180
|
+
provider: "browser-fallback",
|
|
181
|
+
});
|
|
182
|
+
await this.host.emitTelemetry("pie-tool-init-success", {
|
|
183
|
+
toolId: TTS_TOOL_ID,
|
|
184
|
+
operation: "tts-init",
|
|
185
|
+
backend: resolvedBackend,
|
|
186
|
+
provider: "browser-fallback",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (this.host.isDisposalError(error))
|
|
191
|
+
throw error;
|
|
192
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
193
|
+
this.host.reportStartFailure(normalized, { phase: "tts-init" }, [TTS_TOOL_ID]);
|
|
194
|
+
await this.host.emitTelemetry("pie-toolkit-tts-init-error", {
|
|
195
|
+
message: normalized.message,
|
|
196
|
+
});
|
|
197
|
+
await this.host.emitTelemetry("pie-tool-init-error", {
|
|
198
|
+
toolId: TTS_TOOL_ID,
|
|
199
|
+
operation: "tts-init",
|
|
200
|
+
backend: resolvedBackend,
|
|
201
|
+
errorType: "TTSInitError",
|
|
202
|
+
message: normalized.message,
|
|
203
|
+
});
|
|
204
|
+
throw normalized;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
resolveToolConfig() {
|
|
208
|
+
const fromProviders = this.host.providersConfig()?.[TTS_TOOL_ID];
|
|
209
|
+
return fromProviders || {};
|
|
210
|
+
}
|
|
211
|
+
async initializeService(provider, config) {
|
|
212
|
+
const nextProviderOptions = {
|
|
213
|
+
...config.providerOptions,
|
|
214
|
+
__pieTelemetry: async (eventName, payload) => {
|
|
215
|
+
await this.host.emitTelemetry(eventName, {
|
|
216
|
+
toolId: TTS_TOOL_ID,
|
|
217
|
+
...(payload || {}),
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const nextConfig = {
|
|
222
|
+
...config,
|
|
223
|
+
providerOptions: nextProviderOptions,
|
|
224
|
+
};
|
|
225
|
+
await this.host.ttsService.initialize(provider, nextConfig);
|
|
226
|
+
this.host.assertNotDisposed();
|
|
227
|
+
this.host.ttsService.setCatalogResolver(this.host.catalogResolver);
|
|
228
|
+
this.initialized = true;
|
|
229
|
+
this.degraded = false;
|
|
230
|
+
this.host.clearDegradedTool(TTS_TOOL_ID);
|
|
231
|
+
await this.host.hooks.onTTSReady?.();
|
|
232
|
+
this.host.assertNotDisposed();
|
|
233
|
+
}
|
|
234
|
+
reportMissingProvider(backend) {
|
|
235
|
+
// The toolkit this coordinator binds to may still supply the provider.
|
|
236
|
+
if (this.host.isToolRegistryPending()) {
|
|
237
|
+
this.heldMissingProviderBackend = backend;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (this.reportedMissingProvider)
|
|
241
|
+
return;
|
|
242
|
+
this.reportedMissingProvider = true;
|
|
243
|
+
console.warn(`[ToolkitCoordinator] Text-to-speech is configured for the "${backend}" backend but falls back to browser speech: no "${TTS_TOOL_ID}" tool provider is registered. A coordinator registers tool providers only from its \`toolRegistry\`, or from its toolkit's when constructed without one, so supply one that carries the text-to-speech registration — for the packaged capability set, \`createPackagedToolRegistry()\` from "@pie-players/pie-default-tool-loaders". Reported once per coordinator.`);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Report a missing-provider report held back while the registry was
|
|
247
|
+
* unsettled, now that the registry has settled without one.
|
|
248
|
+
*/
|
|
249
|
+
reportHeldMissingProvider() {
|
|
250
|
+
const heldBackend = this.heldMissingProviderBackend;
|
|
251
|
+
this.heldMissingProviderBackend = null;
|
|
252
|
+
if (heldBackend)
|
|
253
|
+
this.reportMissingProvider(heldBackend);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Drop a held missing-provider report without reporting it: a real registry
|
|
257
|
+
* settled, so the stale gap never applied.
|
|
258
|
+
*/
|
|
259
|
+
discardHeldMissingProvider() {
|
|
260
|
+
this.heldMissingProviderBackend = null;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Readiness waits for text-to-speech when it is enabled and either starts
|
|
264
|
+
* eagerly or is granted: a granted read-aloud fails before the learner starts.
|
|
265
|
+
*/
|
|
266
|
+
requiredForReadiness() {
|
|
267
|
+
if (this.resolveToolConfig()?.enabled === false)
|
|
268
|
+
return false;
|
|
269
|
+
return !this.host.lazyInit || this.host.isToolGranted(TTS_TOOL_ID);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Starts text-to-speech for readiness. A failure policy does not grant leaves
|
|
273
|
+
* the tool degraded, reporting itself unavailable, and readiness settles; a
|
|
274
|
+
* granted tool's failure rejects.
|
|
275
|
+
*/
|
|
276
|
+
async startForReadiness() {
|
|
277
|
+
try {
|
|
278
|
+
await this.ensureReady();
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (this.host.isDisposalError(error))
|
|
282
|
+
throw error;
|
|
283
|
+
if (this.host.isToolGranted(TTS_TOOL_ID))
|
|
284
|
+
throw error;
|
|
285
|
+
this.degraded = true;
|
|
286
|
+
this.host.notifyReadyChange();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Re-register the text-to-speech provider for the current config and registry,
|
|
291
|
+
* then re-initialize unless initialization is lazy. An initialization already
|
|
292
|
+
* running finishes first: it would otherwise mark its provider ready after the
|
|
293
|
+
* reset.
|
|
294
|
+
*/
|
|
295
|
+
scheduleReconfigure() {
|
|
296
|
+
const inFlight = this.initPromise;
|
|
297
|
+
const reconfigurePromise = inFlight
|
|
298
|
+
? inFlight.catch(() => { }).then(() => this.reconfigure())
|
|
299
|
+
: this.reconfigure();
|
|
300
|
+
this.reconfigurePromise = reconfigurePromise;
|
|
301
|
+
void reconfigurePromise.finally(() => {
|
|
302
|
+
if (this.reconfigurePromise === reconfigurePromise) {
|
|
303
|
+
this.reconfigurePromise = undefined;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
void reconfigurePromise
|
|
307
|
+
.then(async () => {
|
|
308
|
+
if (this.host.isDisposalStarted())
|
|
309
|
+
return;
|
|
310
|
+
if (this.requiredForReadiness())
|
|
311
|
+
await this.startForReadiness();
|
|
312
|
+
})
|
|
313
|
+
// Reported where it failed; a granted failure has nowhere else to go.
|
|
314
|
+
.catch(() => { });
|
|
315
|
+
}
|
|
316
|
+
async reconfigure() {
|
|
317
|
+
this.initialized = false;
|
|
318
|
+
this.degraded = false;
|
|
319
|
+
this.host.clearDegradedTool(TTS_TOOL_ID);
|
|
320
|
+
this.initPromise = undefined;
|
|
321
|
+
this.host.notifyReadyChange();
|
|
322
|
+
try {
|
|
323
|
+
// The next speak waits on readiness, which starts the new provider.
|
|
324
|
+
this.host.ttsService.releaseProvider();
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// noop: release best effort
|
|
328
|
+
}
|
|
329
|
+
if (this.host.toolProviderRegistry.has(TTS_TOOL_ID)) {
|
|
330
|
+
await this.host.toolProviderRegistry.unregister(TTS_TOOL_ID);
|
|
331
|
+
}
|
|
332
|
+
if (this.host.isDisposalStarted())
|
|
333
|
+
return;
|
|
334
|
+
const ttsRegistration = this.host
|
|
335
|
+
.getProviderDescriptorTools()
|
|
336
|
+
.find((tool) => this.ownsTool(tool.toolId));
|
|
337
|
+
if (!ttsRegistration)
|
|
338
|
+
return;
|
|
339
|
+
await this.host.registerProviderFromTool(ttsRegistration);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host surface ids a content capability can fill.
|
|
3
|
+
*
|
|
4
|
+
* A renderer opens slots in its own layout — a card's docked-media region, the
|
|
5
|
+
* full-width slot above its body — and a registration names the slots it can
|
|
6
|
+
* render into through `ToolRegistration.surfaces`. The ids live here, one
|
|
7
|
+
* definition for both sides of that exchange: a capability package cannot
|
|
8
|
+
* import a player to reach the renderer's constants, and a renderer without a
|
|
9
|
+
* toolbar — print — cannot import the composition layer for the packaged
|
|
10
|
+
* registrations', so the slot names are the toolkit's to own.
|
|
11
|
+
*
|
|
12
|
+
* The ids say nothing about geometry. How wide a slot is, where it sits, or
|
|
13
|
+
* that it collapses under a breakpoint is the renderer's decision; a
|
|
14
|
+
* registration declaring `surfaces` promises only that it can render into a
|
|
15
|
+
* slot with that name, whatever the renderer makes of it.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Host slot for media docked beside a card's content.
|
|
19
|
+
*
|
|
20
|
+
* Named for the relationship rather than for one card kind, because item cards
|
|
21
|
+
* and passage cards open the same slot: an alternate is authored against a
|
|
22
|
+
* content node, and a passage owns content nodes exactly as an item does. A
|
|
23
|
+
* capability declares this surface once and reaches both.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CONTENT_MEDIA_SURFACE = "content-media";
|
|
26
|
+
/**
|
|
27
|
+
* Host slot for a text alternate that has to be read in order with the
|
|
28
|
+
* content: full width, above the card body, in document flow.
|
|
29
|
+
*
|
|
30
|
+
* Separate from {@link CONTENT_MEDIA_SURFACE} because the geometry is the
|
|
31
|
+
* whole difference. Docked media is watched beside the content and is sized by
|
|
32
|
+
* an aspect ratio; a transcript or a plain-language alternate is read *before*
|
|
33
|
+
* the control it belongs to, which a sticky side column cannot express.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CONTENT_LEAD_SURFACE = "content-lead";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host surface ids a content capability can fill.
|
|
3
|
+
*
|
|
4
|
+
* A renderer opens slots in its own layout — a card's docked-media region, the
|
|
5
|
+
* full-width slot above its body — and a registration names the slots it can
|
|
6
|
+
* render into through `ToolRegistration.surfaces`. The ids live here, one
|
|
7
|
+
* definition for both sides of that exchange: a capability package cannot
|
|
8
|
+
* import a player to reach the renderer's constants, and a renderer without a
|
|
9
|
+
* toolbar — print — cannot import the composition layer for the packaged
|
|
10
|
+
* registrations', so the slot names are the toolkit's to own.
|
|
11
|
+
*
|
|
12
|
+
* The ids say nothing about geometry. How wide a slot is, where it sits, or
|
|
13
|
+
* that it collapses under a breakpoint is the renderer's decision; a
|
|
14
|
+
* registration declaring `surfaces` promises only that it can render into a
|
|
15
|
+
* slot with that name, whatever the renderer makes of it.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Host slot for media docked beside a card's content.
|
|
19
|
+
*
|
|
20
|
+
* Named for the relationship rather than for one card kind, because item cards
|
|
21
|
+
* and passage cards open the same slot: an alternate is authored against a
|
|
22
|
+
* content node, and a passage owns content nodes exactly as an item does. A
|
|
23
|
+
* capability declares this surface once and reaches both.
|
|
24
|
+
*/
|
|
25
|
+
export const CONTENT_MEDIA_SURFACE = "content-media";
|
|
26
|
+
/**
|
|
27
|
+
* Host slot for a text alternate that has to be read in order with the
|
|
28
|
+
* content: full width, above the card body, in document flow.
|
|
29
|
+
*
|
|
30
|
+
* Separate from {@link CONTENT_MEDIA_SURFACE} because the geometry is the
|
|
31
|
+
* whole difference. Docked media is watched beside the content and is sized by
|
|
32
|
+
* an aspect ratio; a transcript or a plain-language alternate is read *before*
|
|
33
|
+
* the control it belongs to, which a sticky side column cannot express.
|
|
34
|
+
*/
|
|
35
|
+
export const CONTENT_LEAD_SURFACE = "content-lead";
|
|
@@ -30,6 +30,7 @@ export type { ToolSelectionAction, ToolSelectionContext, } from "../services/sel
|
|
|
30
30
|
export { resolveFallbackToolIcon } from "../services/tool-icons.js";
|
|
31
31
|
export type { ContentCapabilityPhase, ContentCapabilityPolicy, ResolveContentCapabilitiesArgs, ResolvedContentCapability, } from "./content-capability-resolution.js";
|
|
32
32
|
export { resolveContentCapabilities } from "./content-capability-resolution.js";
|
|
33
|
+
export { CONTENT_LEAD_SURFACE, CONTENT_MEDIA_SURFACE, } from "./content-surfaces.js";
|
|
33
34
|
export type { ToolSurfaceHost, ToolSurfaceHostInput, ToolSurfaceHostOptions, ToolSurfaceHostSnapshot, ToolSurfaceScope, } from "./tool-surface-host.js";
|
|
34
35
|
export { createToolSurfaceHost } from "./tool-surface-host.js";
|
|
35
36
|
export type { ElementSupports, ToolContext, } from "../services/tool-context.js";
|
|
@@ -50,6 +51,7 @@ export { findShellScopeHost, resolveContentRegion, } from "../runtime/content-re
|
|
|
50
51
|
export { composedClosest, composedContains, flatTextContent, isShadowRootNode, retargetToTree, } from "../services/tts/flat-tree.js";
|
|
51
52
|
export type { ToolPlacementLevel } from "../services/tools-config-normalizer.js";
|
|
52
53
|
export type { CalculatorToolProviderConfig, TextToSpeechToolProviderConfig, ToolProviderConfig, } from "../services/tools-config-normalizer.js";
|
|
54
|
+
export type { ToolConfigDiagnostic, ToolConfigDiagnosticSeverity, } from "../services/tool-config-validation.js";
|
|
53
55
|
export type { ToolProviderApi } from "../services/tool-providers/ToolProviderApi.js";
|
|
54
56
|
export { TTSToolProvider } from "../services/tool-providers/index.js";
|
|
55
57
|
export type { NormalizedTTSSpeedOption, TTSLayoutMode, TTSSpeedOption, } from "../services/tts-runtime-config.js";
|
|
@@ -29,6 +29,11 @@ export { reportToolFailure } from "../services/tool-failure.js";
|
|
|
29
29
|
// So a gateway button and the toolbar button for the same tool draw one icon.
|
|
30
30
|
export { resolveFallbackToolIcon } from "../services/tool-icons.js";
|
|
31
31
|
export { resolveContentCapabilities } from "./content-capability-resolution.js";
|
|
32
|
+
// The surface ids a registration declares in `surfaces` and a renderer opens in
|
|
33
|
+
// its own layout, one definition for both sides: a capability package cannot
|
|
34
|
+
// import a player, and a renderer with no coordinator — print — cannot import
|
|
35
|
+
// the composition layer for the packaged registrations'.
|
|
36
|
+
export { CONTENT_LEAD_SURFACE, CONTENT_MEDIA_SURFACE, } from "./content-surfaces.js";
|
|
32
37
|
export { createToolSurfaceHost } from "./tool-surface-host.js";
|
|
33
38
|
export { elementSupports, hasMathContent, hasReadableText, hasSpokenContent, hasScienceContent, } from "../services/tool-context.js";
|
|
34
39
|
export { createScopedToolId, parseScopedToolId, } from "../services/tool-instance-id.js";
|