@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
|
@@ -31,9 +31,9 @@ import { FrameworkErrorBus, } from "./framework-error-bus.js";
|
|
|
31
31
|
import { HighlightCoordinator } from "./HighlightCoordinator.js";
|
|
32
32
|
import { ToolCoordinator } from "./ToolCoordinator.js";
|
|
33
33
|
import { TTSService } from "./TTSService.js";
|
|
34
|
-
import { BrowserTTSProvider, browserFallbackConfig, } from "./tts/browser-provider.js";
|
|
35
|
-
import { buildRuntimeTTSConfig, resolveTTSBackend, resolveTTSRuntimeSettings, } from "./tts-runtime-config.js";
|
|
36
34
|
import { ToolProviderRegistry } from "./tool-providers/index.js";
|
|
35
|
+
import { TtsLifecycle, TTS_TOOL_ID, deepMergeMathSpeech, } from "./tts/tts-lifecycle.js";
|
|
36
|
+
import { DEFAULT_TOOL_PROVIDERS } from "./tool-provider-defaults.js";
|
|
37
37
|
import { ToolRegistry } from "./ToolRegistry.js";
|
|
38
38
|
import { ToolRequestRegistry } from "./tool-request.js";
|
|
39
39
|
import { ToolPolicyEngine, } from "../policy/engine.js";
|
|
@@ -52,13 +52,11 @@ class SectionControllerRetiredError extends Error {
|
|
|
52
52
|
this.name = "SectionControllerRetiredError";
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
const isPlainRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
56
55
|
const mergeToolConfigUpdate = (toolId, current, updates) => {
|
|
57
56
|
const next = { ...current, ...updates };
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
next.mathSpeech = { ...current.mathSpeech, ...updates.mathSpeech };
|
|
57
|
+
const deepMathSpeech = deepMergeMathSpeech(toolId, current, updates);
|
|
58
|
+
if (deepMathSpeech) {
|
|
59
|
+
next.mathSpeech = deepMathSpeech;
|
|
62
60
|
}
|
|
63
61
|
return next;
|
|
64
62
|
};
|
|
@@ -136,11 +134,7 @@ export class ToolkitCoordinator {
|
|
|
136
134
|
hooks;
|
|
137
135
|
lazyInit;
|
|
138
136
|
/** Track TTS initialization state */
|
|
139
|
-
|
|
140
|
-
/** Text-to-speech failed to start and policy does not grant it. */
|
|
141
|
-
ttsDegraded = false;
|
|
142
|
-
ttsInitPromise;
|
|
143
|
-
ttsReconfigurePromise;
|
|
137
|
+
tts;
|
|
144
138
|
/**
|
|
145
139
|
* The loader ran, whether or not it returned state. A failed load is
|
|
146
140
|
* reported once and the coordinator runs without saved tool state.
|
|
@@ -243,18 +237,6 @@ export class ToolkitCoordinator {
|
|
|
243
237
|
* lifetime for no new information.
|
|
244
238
|
*/
|
|
245
239
|
reportedUnboundFeaturePolicy = false;
|
|
246
|
-
/**
|
|
247
|
-
* Whether {@link _initializeTTS} has already reported a non-browser backend
|
|
248
|
-
* falling back to browser speech because no text-to-speech provider is registered.
|
|
249
|
-
* Once per coordinator: a text-to-speech config change re-runs
|
|
250
|
-
* initialization, and the missing provider is the same gap each time.
|
|
251
|
-
*/
|
|
252
|
-
reportedMissingTTSProvider = false;
|
|
253
|
-
/**
|
|
254
|
-
* The backend of a fallback {@link reportMissingTTSProvider} held back while
|
|
255
|
-
* the registry was unsettled, reported if the bound toolkit has no registry.
|
|
256
|
-
*/
|
|
257
|
-
heldMissingTTSProviderBackend = null;
|
|
258
240
|
/**
|
|
259
241
|
* Whether {@link validateToolsConfig} has already reported validating with
|
|
260
242
|
* no registry (`tools.registryUnavailable`). Once per coordinator: every
|
|
@@ -278,13 +260,7 @@ export class ToolkitCoordinator {
|
|
|
278
260
|
toolRegistry,
|
|
279
261
|
});
|
|
280
262
|
const defaultProviders = {
|
|
281
|
-
|
|
282
|
-
enabled: true,
|
|
283
|
-
backend: "browser",
|
|
284
|
-
},
|
|
285
|
-
annotationToolbar: {
|
|
286
|
-
enabled: true,
|
|
287
|
-
},
|
|
263
|
+
...DEFAULT_TOOL_PROVIDERS,
|
|
288
264
|
};
|
|
289
265
|
return {
|
|
290
266
|
...config,
|
|
@@ -358,7 +334,27 @@ export class ToolkitCoordinator {
|
|
|
358
334
|
// A caller that speaks without `ensureTTSReady()` first, such as selection
|
|
359
335
|
// read-aloud, starts the service instead of finding it uninitialized.
|
|
360
336
|
this.ttsService.setReadinessGate(() => this.ensureTTSReady());
|
|
361
|
-
this.
|
|
337
|
+
this.tts = new TtsLifecycle({
|
|
338
|
+
assertNotDisposed: () => this.assertNotDisposed(),
|
|
339
|
+
isDisposalStarted: () => this.disposePromise !== null,
|
|
340
|
+
isDisposalError: (error) => error instanceof ToolkitCoordinatorDisposedError,
|
|
341
|
+
emitTelemetry: (eventName, payload) => this.emitTelemetry(eventName, payload),
|
|
342
|
+
hooks: this.hooks,
|
|
343
|
+
toolProviderRegistry: this.toolProviderRegistry,
|
|
344
|
+
initializeProvider: (toolId, fallbackFollows) => this.initializeProvider(toolId, fallbackFollows),
|
|
345
|
+
ttsService: this.ttsService,
|
|
346
|
+
catalogResolver: this.catalogResolver,
|
|
347
|
+
clearDegradedTool: (toolId) => void this.degradedTools.delete(toolId),
|
|
348
|
+
reportStartFailure: (error, context, toolIds) => this.reportStartFailure(error, context, toolIds),
|
|
349
|
+
isToolGranted: (toolId) => this.isToolGranted(toolId),
|
|
350
|
+
lazyInit: this.lazyInit,
|
|
351
|
+
isToolRegistryPending: () => this.toolRegistryPending(),
|
|
352
|
+
providersConfig: () => this.config.tools?.providers,
|
|
353
|
+
getProviderDescriptorTools: () => this.getProviderDescriptorTools(),
|
|
354
|
+
registerProviderFromTool: (tool, replace) => this.registerProviderFromTool(tool, replace),
|
|
355
|
+
notifyReadyChange: () => this.notifyReadyChange(),
|
|
356
|
+
});
|
|
357
|
+
this.ttsService.setMathSpeechSource(() => this.tts.mathSpeechRuntimeOptions());
|
|
362
358
|
this.setupStatePersistenceHooks();
|
|
363
359
|
// The unified ToolPolicyEngine, seeded with the validated tools config.
|
|
364
360
|
// The assessment starts `null` and items register their settings as they
|
|
@@ -724,17 +720,6 @@ export class ToolkitCoordinator {
|
|
|
724
720
|
toolRegistryPending() {
|
|
725
721
|
return !this.toolRegistrySettled && !this.toolRegistryAbsent;
|
|
726
722
|
}
|
|
727
|
-
reportMissingTTSProvider(backend) {
|
|
728
|
-
// The toolkit this coordinator binds to may still supply the provider.
|
|
729
|
-
if (this.toolRegistryPending()) {
|
|
730
|
-
this.heldMissingTTSProviderBackend = backend;
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
if (this.reportedMissingTTSProvider)
|
|
734
|
-
return;
|
|
735
|
-
this.reportedMissingTTSProvider = true;
|
|
736
|
-
console.warn(`[ToolkitCoordinator] Text-to-speech is configured for the "${backend}" backend but falls back to browser speech: no "textToSpeech" 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.`);
|
|
737
|
-
}
|
|
738
723
|
/**
|
|
739
724
|
* Take the registry of the toolkit this coordinator is bound to, when the host
|
|
740
725
|
* constructed it without one: the config is validated against it, its
|
|
@@ -751,8 +736,6 @@ export class ToolkitCoordinator {
|
|
|
751
736
|
return false;
|
|
752
737
|
if (!registry && this.toolRegistryAbsent)
|
|
753
738
|
return false;
|
|
754
|
-
const heldBackend = this.heldMissingTTSProviderBackend;
|
|
755
|
-
this.heldMissingTTSProviderBackend = null;
|
|
756
739
|
const strictness = this.config.toolConfigStrictness ?? "error";
|
|
757
740
|
const source = "ToolkitCoordinator.adoptToolRegistry";
|
|
758
741
|
if (!registry) {
|
|
@@ -762,13 +745,13 @@ export class ToolkitCoordinator {
|
|
|
762
745
|
source,
|
|
763
746
|
toolRegistry: this.toolRegistry,
|
|
764
747
|
});
|
|
765
|
-
|
|
766
|
-
this.reportMissingTTSProvider(heldBackend);
|
|
748
|
+
this.tts.reportHeldMissingProvider();
|
|
767
749
|
return false;
|
|
768
750
|
}
|
|
769
751
|
this.toolRegistrySettled = true;
|
|
770
752
|
this.toolRegistry = registry;
|
|
771
753
|
this.config.toolRegistry = registry;
|
|
754
|
+
this.tts.discardHeldMissingProvider();
|
|
772
755
|
try {
|
|
773
756
|
// Warn at most: the config was accepted at construction, and a binding
|
|
774
757
|
// toolkit has no caller to throw to.
|
|
@@ -782,16 +765,14 @@ export class ToolkitCoordinator {
|
|
|
782
765
|
console.warn("[ToolkitCoordinator] Tool config does not validate against the adopted tool registry:", err);
|
|
783
766
|
}
|
|
784
767
|
this.policyEngine.replaceToolRegistry(registry, this.config.tools);
|
|
785
|
-
const ttsStarted = this.
|
|
786
|
-
this.ttsInitPromise !== undefined ||
|
|
787
|
-
this.ttsReconfigurePromise !== undefined;
|
|
768
|
+
const ttsStarted = this.tts.isStarted();
|
|
788
769
|
for (const tool of this.getProviderDescriptorTools()) {
|
|
789
|
-
if (ttsStarted && tool.toolId
|
|
770
|
+
if (ttsStarted && this.tts.ownsTool(tool.toolId))
|
|
790
771
|
continue;
|
|
791
772
|
void this.registerProviderFromTool(tool);
|
|
792
773
|
}
|
|
793
774
|
if (ttsStarted)
|
|
794
|
-
this.
|
|
775
|
+
this.tts.scheduleReconfigure();
|
|
795
776
|
return true;
|
|
796
777
|
}
|
|
797
778
|
_registerToolProviders() {
|
|
@@ -1738,159 +1719,19 @@ export class ToolkitCoordinator {
|
|
|
1738
1719
|
if (this.coordinatorReadyPromise) {
|
|
1739
1720
|
pending.add(this.coordinatorReadyPromise);
|
|
1740
1721
|
}
|
|
1741
|
-
|
|
1742
|
-
pending.add(this.ttsInitPromise);
|
|
1743
|
-
if (this.ttsReconfigurePromise) {
|
|
1744
|
-
pending.add(this.ttsReconfigurePromise);
|
|
1745
|
-
}
|
|
1722
|
+
this.tts.flushInitPromises(pending);
|
|
1746
1723
|
for (const promise of this.providerInitPromises.values()) {
|
|
1747
1724
|
pending.add(promise);
|
|
1748
1725
|
}
|
|
1749
1726
|
await settleAll(pending);
|
|
1750
1727
|
}
|
|
1751
1728
|
/**
|
|
1752
|
-
* Initialize the TTS service from the `textToSpeech` tool config.
|
|
1729
|
+
* Initialize the TTS service from the `textToSpeech` tool config. The
|
|
1730
|
+
* lifecycle itself lives in `services/tts/tts-lifecycle.ts`; the coordinator
|
|
1731
|
+
* keeps the public method hosts call.
|
|
1753
1732
|
*/
|
|
1754
1733
|
async ensureTTSReady() {
|
|
1755
|
-
this.
|
|
1756
|
-
await this.waitForPendingTTSReconfigure();
|
|
1757
|
-
this.assertNotDisposed();
|
|
1758
|
-
if (this.ttsInitialized)
|
|
1759
|
-
return;
|
|
1760
|
-
if (this.ttsInitPromise)
|
|
1761
|
-
return this.ttsInitPromise;
|
|
1762
|
-
this.ttsInitPromise = this._initializeTTS()
|
|
1763
|
-
.then(() => this.assertNotDisposed())
|
|
1764
|
-
.finally(() => {
|
|
1765
|
-
this.ttsInitPromise = undefined;
|
|
1766
|
-
this.notifyReadyChange();
|
|
1767
|
-
});
|
|
1768
|
-
return this.ttsInitPromise;
|
|
1769
|
-
}
|
|
1770
|
-
async waitForPendingTTSReconfigure() {
|
|
1771
|
-
let pending = this.ttsReconfigurePromise;
|
|
1772
|
-
while (pending) {
|
|
1773
|
-
await pending;
|
|
1774
|
-
pending = this.ttsReconfigurePromise;
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
async _initializeTTS() {
|
|
1778
|
-
if (this.ttsInitialized)
|
|
1779
|
-
return;
|
|
1780
|
-
const resolvedToolConfig = this.resolveTTSToolConfig();
|
|
1781
|
-
const runtimeSettings = resolveTTSRuntimeSettings(resolvedToolConfig);
|
|
1782
|
-
const resolvedBackend = resolveTTSBackend(runtimeSettings);
|
|
1783
|
-
const runtimeTTSConfig = buildRuntimeTTSConfig(runtimeSettings);
|
|
1784
|
-
await this.hooks.onBeforeTTSInit?.({
|
|
1785
|
-
phase: "tts-init",
|
|
1786
|
-
details: {
|
|
1787
|
-
backend: resolvedBackend,
|
|
1788
|
-
},
|
|
1789
|
-
});
|
|
1790
|
-
this.assertNotDisposed();
|
|
1791
|
-
await this.emitTelemetry("pie-toolkit-tts-init-start", {
|
|
1792
|
-
backend: resolvedBackend,
|
|
1793
|
-
});
|
|
1794
|
-
await this.emitTelemetry("pie-tool-init-start", {
|
|
1795
|
-
toolId: "textToSpeech",
|
|
1796
|
-
operation: "tts-init",
|
|
1797
|
-
backend: resolvedBackend,
|
|
1798
|
-
});
|
|
1799
|
-
if (this.toolProviderRegistry.has("textToSpeech")) {
|
|
1800
|
-
try {
|
|
1801
|
-
// Browser speech follows a server provider that fails to start.
|
|
1802
|
-
const ttsProvider = await this.initializeProvider("textToSpeech", true);
|
|
1803
|
-
const providerInstance = await ttsProvider.createInstance();
|
|
1804
|
-
await this.initializeTTSService(providerInstance, runtimeTTSConfig);
|
|
1805
|
-
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
1806
|
-
provider: "registry",
|
|
1807
|
-
});
|
|
1808
|
-
await this.emitTelemetry("pie-tool-init-success", {
|
|
1809
|
-
toolId: "textToSpeech",
|
|
1810
|
-
operation: "tts-init",
|
|
1811
|
-
backend: resolvedBackend,
|
|
1812
|
-
provider: "registry",
|
|
1813
|
-
});
|
|
1814
|
-
return;
|
|
1815
|
-
}
|
|
1816
|
-
catch (error) {
|
|
1817
|
-
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1818
|
-
throw error;
|
|
1819
|
-
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1820
|
-
await this.emitTelemetry("pie-tool-init-fallback", {
|
|
1821
|
-
toolId: "textToSpeech",
|
|
1822
|
-
operation: "tts-init",
|
|
1823
|
-
backend: resolvedBackend,
|
|
1824
|
-
fromProvider: "registry",
|
|
1825
|
-
toProvider: "browser",
|
|
1826
|
-
reason: normalized.message,
|
|
1827
|
-
});
|
|
1828
|
-
console.warn("[ToolkitCoordinator] Failed to initialize TTS via registry, falling back to browser provider:", normalized);
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
else if (resolvedBackend !== "browser") {
|
|
1832
|
-
this.reportMissingTTSProvider(resolvedBackend);
|
|
1833
|
-
}
|
|
1834
|
-
// Browser speech, configured or standing in for a backend that has no
|
|
1835
|
-
// provider or failed to start.
|
|
1836
|
-
const provider = new BrowserTTSProvider();
|
|
1837
|
-
try {
|
|
1838
|
-
await this.initializeTTSService(provider, resolvedBackend === "browser"
|
|
1839
|
-
? runtimeTTSConfig
|
|
1840
|
-
: browserFallbackConfig(runtimeTTSConfig));
|
|
1841
|
-
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
1842
|
-
provider: "browser-fallback",
|
|
1843
|
-
});
|
|
1844
|
-
await this.emitTelemetry("pie-tool-init-success", {
|
|
1845
|
-
toolId: "textToSpeech",
|
|
1846
|
-
operation: "tts-init",
|
|
1847
|
-
backend: resolvedBackend,
|
|
1848
|
-
provider: "browser-fallback",
|
|
1849
|
-
});
|
|
1850
|
-
}
|
|
1851
|
-
catch (error) {
|
|
1852
|
-
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1853
|
-
throw error;
|
|
1854
|
-
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1855
|
-
this.reportStartFailure(normalized, { phase: "tts-init" }, ["textToSpeech"]);
|
|
1856
|
-
await this.emitTelemetry("pie-toolkit-tts-init-error", {
|
|
1857
|
-
message: normalized.message,
|
|
1858
|
-
});
|
|
1859
|
-
await this.emitTelemetry("pie-tool-init-error", {
|
|
1860
|
-
toolId: "textToSpeech",
|
|
1861
|
-
operation: "tts-init",
|
|
1862
|
-
backend: resolvedBackend,
|
|
1863
|
-
errorType: "TTSInitError",
|
|
1864
|
-
message: normalized.message,
|
|
1865
|
-
});
|
|
1866
|
-
throw normalized;
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
resolveTTSToolConfig() {
|
|
1870
|
-
return this.getTTSConfigFromProviders() || {};
|
|
1871
|
-
}
|
|
1872
|
-
async initializeTTSService(provider, config) {
|
|
1873
|
-
const nextProviderOptions = {
|
|
1874
|
-
...config.providerOptions,
|
|
1875
|
-
__pieTelemetry: async (eventName, payload) => {
|
|
1876
|
-
await this.emitTelemetry(eventName, {
|
|
1877
|
-
toolId: "textToSpeech",
|
|
1878
|
-
...(payload || {}),
|
|
1879
|
-
});
|
|
1880
|
-
},
|
|
1881
|
-
};
|
|
1882
|
-
const nextConfig = {
|
|
1883
|
-
...config,
|
|
1884
|
-
providerOptions: nextProviderOptions,
|
|
1885
|
-
};
|
|
1886
|
-
await this.ttsService.initialize(provider, nextConfig);
|
|
1887
|
-
this.assertNotDisposed();
|
|
1888
|
-
this.ttsService.setCatalogResolver(this.catalogResolver);
|
|
1889
|
-
this.ttsInitialized = true;
|
|
1890
|
-
this.ttsDegraded = false;
|
|
1891
|
-
this.degradedTools.delete("textToSpeech");
|
|
1892
|
-
await this.hooks.onTTSReady?.();
|
|
1893
|
-
this.assertNotDisposed();
|
|
1734
|
+
return this.tts.ensureReady();
|
|
1894
1735
|
}
|
|
1895
1736
|
async waitUntilReady() {
|
|
1896
1737
|
this.assertNotDisposed();
|
|
@@ -1901,8 +1742,8 @@ export class ToolkitCoordinator {
|
|
|
1901
1742
|
this.coordinatorReadyPromise = (async () => {
|
|
1902
1743
|
await this.ensureStateLoaded();
|
|
1903
1744
|
this.assertNotDisposed();
|
|
1904
|
-
if (this.
|
|
1905
|
-
await this.
|
|
1745
|
+
if (this.tts.requiredForReadiness()) {
|
|
1746
|
+
await this.tts.startForReadiness();
|
|
1906
1747
|
}
|
|
1907
1748
|
this.assertNotDisposed();
|
|
1908
1749
|
if (!this.coordinatorReadyNotified) {
|
|
@@ -1920,39 +1761,9 @@ export class ToolkitCoordinator {
|
|
|
1920
1761
|
}
|
|
1921
1762
|
isReady() {
|
|
1922
1763
|
return ((this.stateLoadSettled || !this.hooks.loadToolState) &&
|
|
1923
|
-
(this.
|
|
1924
|
-
this.
|
|
1925
|
-
!this.
|
|
1926
|
-
}
|
|
1927
|
-
/**
|
|
1928
|
-
* Readiness waits for text-to-speech when it is enabled and either starts
|
|
1929
|
-
* eagerly or is granted: a granted read-aloud fails before the learner starts.
|
|
1930
|
-
*/
|
|
1931
|
-
ttsRequiredForReadiness() {
|
|
1932
|
-
if (this.getTTSConfigFromProviders()?.enabled === false)
|
|
1933
|
-
return false;
|
|
1934
|
-
return !this.lazyInit || this.isToolGranted("textToSpeech");
|
|
1935
|
-
}
|
|
1936
|
-
/**
|
|
1937
|
-
* Starts text-to-speech for readiness. A failure policy does not grant leaves
|
|
1938
|
-
* the tool degraded, reporting itself unavailable, and readiness settles; a
|
|
1939
|
-
* granted tool's failure rejects.
|
|
1940
|
-
*/
|
|
1941
|
-
async startTTSForReadiness() {
|
|
1942
|
-
try {
|
|
1943
|
-
await this.ensureTTSReady();
|
|
1944
|
-
}
|
|
1945
|
-
catch (error) {
|
|
1946
|
-
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1947
|
-
throw error;
|
|
1948
|
-
if (this.isToolGranted("textToSpeech"))
|
|
1949
|
-
throw error;
|
|
1950
|
-
this.ttsDegraded = true;
|
|
1951
|
-
this.notifyReadyChange();
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
getTTSConfigFromProviders() {
|
|
1955
|
-
return this.config.tools?.providers?.textToSpeech;
|
|
1764
|
+
(this.tts.started ||
|
|
1765
|
+
this.tts.failedUngranted ||
|
|
1766
|
+
!this.tts.requiredForReadiness()));
|
|
1956
1767
|
}
|
|
1957
1768
|
assertCanonicalToolId(toolId) {
|
|
1958
1769
|
if (typeof toolId !== "string" || toolId.trim().length === 0) {
|
|
@@ -2360,8 +2171,8 @@ export class ToolkitCoordinator {
|
|
|
2360
2171
|
_applyToolConfigChange(toolId) {
|
|
2361
2172
|
if (this.disposePromise !== null)
|
|
2362
2173
|
return;
|
|
2363
|
-
if (toolId
|
|
2364
|
-
this.
|
|
2174
|
+
if (this.tts.ownsTool(toolId)) {
|
|
2175
|
+
this.tts.scheduleReconfigure();
|
|
2365
2176
|
return;
|
|
2366
2177
|
}
|
|
2367
2178
|
this._registerChangedToolProvider(toolId);
|
|
@@ -2379,54 +2190,4 @@ export class ToolkitCoordinator {
|
|
|
2379
2190
|
this.degradedTools.delete(toolId);
|
|
2380
2191
|
void this.registerProviderFromTool(registration, true);
|
|
2381
2192
|
}
|
|
2382
|
-
/**
|
|
2383
|
-
* Re-register the text-to-speech provider for the current config and registry, then
|
|
2384
|
-
* re-initialize unless initialization is lazy. An initialization already
|
|
2385
|
-
* running finishes first: it would otherwise mark its provider ready after
|
|
2386
|
-
* the reset.
|
|
2387
|
-
*/
|
|
2388
|
-
scheduleTTSReconfigure() {
|
|
2389
|
-
const inFlight = this.ttsInitPromise;
|
|
2390
|
-
const reconfigurePromise = inFlight
|
|
2391
|
-
? inFlight.catch(() => { }).then(() => this._reconfigureTTSProvider())
|
|
2392
|
-
: this._reconfigureTTSProvider();
|
|
2393
|
-
this.ttsReconfigurePromise = reconfigurePromise;
|
|
2394
|
-
void reconfigurePromise.finally(() => {
|
|
2395
|
-
if (this.ttsReconfigurePromise === reconfigurePromise) {
|
|
2396
|
-
this.ttsReconfigurePromise = undefined;
|
|
2397
|
-
}
|
|
2398
|
-
});
|
|
2399
|
-
void reconfigurePromise
|
|
2400
|
-
.then(async () => {
|
|
2401
|
-
if (this.disposePromise !== null)
|
|
2402
|
-
return;
|
|
2403
|
-
if (this.ttsRequiredForReadiness())
|
|
2404
|
-
await this.startTTSForReadiness();
|
|
2405
|
-
})
|
|
2406
|
-
// Reported where it failed; a granted failure has nowhere else to go.
|
|
2407
|
-
.catch(() => { });
|
|
2408
|
-
}
|
|
2409
|
-
async _reconfigureTTSProvider() {
|
|
2410
|
-
this.ttsInitialized = false;
|
|
2411
|
-
this.ttsDegraded = false;
|
|
2412
|
-
this.degradedTools.delete("textToSpeech");
|
|
2413
|
-
this.ttsInitPromise = undefined;
|
|
2414
|
-
this.notifyReadyChange();
|
|
2415
|
-
try {
|
|
2416
|
-
// The next speak waits on readiness, which starts the new provider.
|
|
2417
|
-
this.ttsService.releaseProvider();
|
|
2418
|
-
}
|
|
2419
|
-
catch {
|
|
2420
|
-
// noop: release best effort
|
|
2421
|
-
}
|
|
2422
|
-
if (this.toolProviderRegistry.has("textToSpeech")) {
|
|
2423
|
-
await this.toolProviderRegistry.unregister("textToSpeech");
|
|
2424
|
-
}
|
|
2425
|
-
if (this.disposePromise !== null)
|
|
2426
|
-
return;
|
|
2427
|
-
const ttsRegistration = this.getProviderDescriptorTools().find((tool) => tool.toolId === "textToSpeech");
|
|
2428
|
-
if (!ttsRegistration)
|
|
2429
|
-
return;
|
|
2430
|
-
await this.registerProviderFromTool(ttsRegistration);
|
|
2431
|
-
}
|
|
2432
2193
|
}
|
|
@@ -270,6 +270,26 @@ export type SectionControllerTimedMediaInvalidEvent = SectionControllerEventBase
|
|
|
270
270
|
};
|
|
271
271
|
export type SectionControllerEvent = SectionControllerItemSessionDataChangedEvent | SectionControllerItemSessionMetaChangedEvent | SectionControllerItemSelectedEvent | SectionControllerSectionNavigationChangedEvent | SectionControllerSectionSessionAppliedEvent | SectionControllerContentLoadedEvent | SectionControllerItemPlayerErrorEvent | SectionControllerItemCompleteChangedEvent | SectionControllerSectionLoadingCompleteEvent | SectionControllerSectionItemsCompleteChangedEvent | SectionControllerSectionErrorEvent | SectionControllerFormativeTryRecordedEvent | SectionControllerFormativeRevealChangedEvent | SectionControllerSectionMasteryChangedEvent | SectionControllerTimedMediaCueChangedEvent | SectionControllerTimedMediaAudioStartedEvent | SectionControllerTimedMediaPolicyDegradedEvent | SectionControllerTimedMediaInvalidEvent;
|
|
272
272
|
export type SectionControllerEventType = SectionControllerEvent["type"];
|
|
273
|
+
/** One item's part of a {@link SectionScore}. */
|
|
274
|
+
export type SectionItemScore = {
|
|
275
|
+
canonicalItemId: string;
|
|
276
|
+
/**
|
|
277
|
+
* The item's element outcomes, one slot per scored model as
|
|
278
|
+
* `pie-item-player.provideScore()` returns them. `null` while the item is not
|
|
279
|
+
* rendered, or when it has no scorable model.
|
|
280
|
+
*/
|
|
281
|
+
outcomes: Array<Record<string, unknown> | undefined> | null;
|
|
282
|
+
};
|
|
283
|
+
/** The section's browser score, from {@link SectionControllerHandle.score}. */
|
|
284
|
+
export type SectionScore = {
|
|
285
|
+
/** Every item in the section, in section order. */
|
|
286
|
+
items: SectionItemScore[];
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Scores one rendered item in the browser: its element outcomes, or `false` when
|
|
290
|
+
* the item is not rendered or has no scorable model.
|
|
291
|
+
*/
|
|
292
|
+
export type SectionItemScorer = (canonicalItemId: string) => Promise<false | Array<Record<string, unknown> | undefined>>;
|
|
273
293
|
/**
|
|
274
294
|
* Host-facing controller for a single section cohort.
|
|
275
295
|
*
|
|
@@ -422,6 +442,26 @@ export interface SectionControllerHandle {
|
|
|
422
442
|
* those boundaries.
|
|
423
443
|
*/
|
|
424
444
|
setPendingSessionCommit?(commit: (() => void) | null): void;
|
|
445
|
+
/**
|
|
446
|
+
* Commit every element's pending response to the section session, so the
|
|
447
|
+
* next `getSession()` holds it. For a host about to read or submit the
|
|
448
|
+
* session; the controller already commits before its own boundaries.
|
|
449
|
+
*/
|
|
450
|
+
commitPendingSessions?(): void;
|
|
451
|
+
/**
|
|
452
|
+
* Register how the controller scores a rendered item. The controller is
|
|
453
|
+
* DOM-free, so the layout that renders the item players supplies it, as with
|
|
454
|
+
* `setPendingSessionCommit`. The latest registration wins; the returned
|
|
455
|
+
* release unregisters this scorer and leaves a later one in place, so two
|
|
456
|
+
* layouts on one controller do not unregister each other.
|
|
457
|
+
*/
|
|
458
|
+
registerItemScorer?(scorer: SectionItemScorer): () => void;
|
|
459
|
+
/**
|
|
460
|
+
* Score the section in the browser: each item's element outcomes, in section
|
|
461
|
+
* order. An item that is not rendered, or has no scorable model, scores
|
|
462
|
+
* `null`.
|
|
463
|
+
*/
|
|
464
|
+
score?(): Promise<SectionScore>;
|
|
425
465
|
/**
|
|
426
466
|
* Record one Try for an item: derive correctness from the element outcomes,
|
|
427
467
|
* increment the Try count, and reveal feedback if the item's resolved
|
|
@@ -3,7 +3,7 @@ import type { ToolRegistry } from "./ToolRegistry.js";
|
|
|
3
3
|
export type ToolConfigStrictness = "off" | "warn" | "error";
|
|
4
4
|
export type ToolConfigDiagnosticSeverity = "warning" | "error";
|
|
5
5
|
export interface ToolConfigDiagnostic {
|
|
6
|
-
code: "tools.unknownToolId" | "tools.unsupportedLevel" | "tools.unplaceableActivation" | "tools.registryUnavailable" | "tools.unknownProviderKey" | "tools.providerSanitizeFailed" | "tools.providerValidateFailed" | "tools.invalidProviderValidation";
|
|
6
|
+
code: "tools.unknownToolId" | "tools.unsupportedLevel" | "tools.unplaceableActivation" | "tools.registryUnavailable" | "tools.unknownProviderKey" | "tools.providerSanitizeFailed" | "tools.providerValidateFailed" | "tools.invalidProviderValidation" | "tools.invalidProviderConfig";
|
|
7
7
|
severity: ToolConfigDiagnosticSeverity;
|
|
8
8
|
path: string;
|
|
9
9
|
message: string;
|
|
@@ -31,6 +31,45 @@ function isElementContext(context) {
|
|
|
31
31
|
return context.level === "element";
|
|
32
32
|
}
|
|
33
33
|
const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").trim();
|
|
34
|
+
// The digit fold mirrors pie-elements-ng's response normalizer
|
|
35
|
+
// (`controller-utils/src/response-normalizer.ts`), so tool visibility and
|
|
36
|
+
// scoring read one number the same way. The separators fold to the
|
|
37
|
+
// point-decimal marks the patterns below are written in.
|
|
38
|
+
const DECIMAL_DIGIT = /\p{Nd}/u;
|
|
39
|
+
const NON_ASCII_DIGITS = /(?![0-9])\p{Nd}/gu;
|
|
40
|
+
// ALM, LRM, RLM, the embeddings and overrides, the isolates, and tatweel.
|
|
41
|
+
const BIDI_CONTROLS_AND_TATWEEL = /[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069\u0640]/g;
|
|
42
|
+
const digitValues = new Map();
|
|
43
|
+
/**
|
|
44
|
+
* A decimal digit's value: its offset into its run of ten. Unicode assigns
|
|
45
|
+
* decimal digits only in ascending runs from zero, and runs can abut, hence the
|
|
46
|
+
* modulo.
|
|
47
|
+
*/
|
|
48
|
+
function digitValue(codePoint) {
|
|
49
|
+
const cached = digitValues.get(codePoint);
|
|
50
|
+
if (cached !== undefined)
|
|
51
|
+
return cached;
|
|
52
|
+
let offset = 0;
|
|
53
|
+
while (DECIMAL_DIGIT.test(String.fromCodePoint(codePoint - offset - 1))) {
|
|
54
|
+
offset += 1;
|
|
55
|
+
}
|
|
56
|
+
const value = offset % 10;
|
|
57
|
+
digitValues.set(codePoint, value);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Text with every decimal digit in ASCII, the Arabic decimal, thousands and
|
|
62
|
+
* percent signs as `.`, `,` and `%`, and bidi controls and tatweel removed, so
|
|
63
|
+
* the patterns below match `٣٫٥ × ٢ = ٧` as they match `3.5 × 2 = 7`.
|
|
64
|
+
*/
|
|
65
|
+
function foldNumerals(text) {
|
|
66
|
+
return text
|
|
67
|
+
.replace(BIDI_CONTROLS_AND_TATWEEL, "")
|
|
68
|
+
.replace(NON_ASCII_DIGITS, (digit) => String(digitValue(digit.codePointAt(0))))
|
|
69
|
+
.replace(/\u066B/g, ".")
|
|
70
|
+
.replace(/\u066C/g, ",")
|
|
71
|
+
.replace(/\u066A/g, "%");
|
|
72
|
+
}
|
|
34
73
|
/**
|
|
35
74
|
* A config's `models` as a list, whether it was authored as an array or as a
|
|
36
75
|
* record keyed by element id. Both forms are in the wild.
|
|
@@ -155,6 +194,10 @@ function extractContent(context, transform) {
|
|
|
155
194
|
function extractTextContent(context) {
|
|
156
195
|
return extractContent(context, stripHtml);
|
|
157
196
|
}
|
|
197
|
+
/** The plain text a context carries, with its numerals folded for matching. */
|
|
198
|
+
function extractMatchableText(context) {
|
|
199
|
+
return foldNumerals(extractTextContent(context));
|
|
200
|
+
}
|
|
158
201
|
/**
|
|
159
202
|
* The authored markup a context carries, tags intact.
|
|
160
203
|
*
|
|
@@ -250,10 +293,24 @@ function hasSpacedSubtraction(text) {
|
|
|
250
293
|
});
|
|
251
294
|
}
|
|
252
295
|
const PERCENTAGE = /\d\s*%/;
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
296
|
+
/**
|
|
297
|
+
* A percentage taken of a number: `20% of 50`, `15% of $80`, `el 20% de 50`,
|
|
298
|
+
* `٢٠٪ من ٥٠`.
|
|
299
|
+
*/
|
|
300
|
+
const PERCENTAGE_OF_NUMBER = /\d\s*%\s+(?:of|de|van|من)\s+\p{Sc}?\s?\d/iu;
|
|
301
|
+
/** A money amount: `$40`, `€ 12`, `12,50 €`, `٥٠ ؋`. */
|
|
302
|
+
const MONEY = /\p{Sc}\s?\d|\d\s?\p{Sc}/u;
|
|
303
|
+
/**
|
|
304
|
+
* Currencies written as a word after the amount, by content language.
|
|
305
|
+
*
|
|
306
|
+
* Arabic writes most currencies as a word or an abbreviation where English
|
|
307
|
+
* writes a sign: `٥٠ ريالًا`, `١٢ د.إ`. English has no set, because its
|
|
308
|
+
* spelled-out "40 dollars" never counted. Script-disjoint, so every set is
|
|
309
|
+
* tried. The Arabic set is pending the native review (PIE-1364).
|
|
310
|
+
*/
|
|
311
|
+
const CURRENCY_WORDS = {
|
|
312
|
+
ar: /\d\s*(?:ريال|دينار|درهم|جنيه|دولار|يورو|ليرة|ر\.س|د\.إ|د\.ك|ج\.م)/u,
|
|
313
|
+
};
|
|
257
314
|
/**
|
|
258
315
|
* Whether the text applies a percentage to a quantity.
|
|
259
316
|
*
|
|
@@ -265,7 +322,10 @@ const MONEY = /[$€£]\s?\d|\d\s?[€£]/;
|
|
|
265
322
|
function hasAppliedPercentage(text) {
|
|
266
323
|
if (PERCENTAGE_OF_NUMBER.test(text))
|
|
267
324
|
return true;
|
|
268
|
-
|
|
325
|
+
if (!PERCENTAGE.test(text))
|
|
326
|
+
return false;
|
|
327
|
+
return (MONEY.test(text) ||
|
|
328
|
+
Object.values(CURRENCY_WORDS).some((pattern) => pattern.test(text)));
|
|
269
329
|
}
|
|
270
330
|
/**
|
|
271
331
|
* Helper to check if context contains mathematical content
|
|
@@ -283,7 +343,7 @@ export function hasMathContent(context) {
|
|
|
283
343
|
];
|
|
284
344
|
if (structuralIndicators.some((pattern) => pattern.test(markup)))
|
|
285
345
|
return true;
|
|
286
|
-
const text =
|
|
346
|
+
const text = extractMatchableText(context);
|
|
287
347
|
// No bare-operator pattern: a lone hyphen or slash matches "well-known" and
|
|
288
348
|
// "and/or", which would make nearly every item mathematical. An operator
|
|
289
349
|
// counts only with operands around it, or when the character has no prose
|
|
@@ -399,16 +459,39 @@ export function hasSpokenContent(context) {
|
|
|
399
459
|
* (Basic heuristic - can be overridden by tools)
|
|
400
460
|
*/
|
|
401
461
|
export function hasScienceContent(context) {
|
|
402
|
-
const text =
|
|
462
|
+
const text = extractMatchableText(context);
|
|
403
463
|
if (hasChemicalFormula(text))
|
|
404
464
|
return true;
|
|
405
465
|
if (/[A-Z][a-z]?[\u2080-\u2089]/.test(text))
|
|
406
466
|
return true; // Subscripted: H₂O, CO₂
|
|
407
|
-
|
|
467
|
+
return Object.values(SCIENCE_KEYWORDS).some((patterns) => patterns.some((pattern) => pattern.test(text)));
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Science vocabulary by content language, one pattern per field: chemistry,
|
|
471
|
+
* element naming, biology, physics.
|
|
472
|
+
*
|
|
473
|
+
* Every set is tried, because the sets are script-disjoint and content often
|
|
474
|
+
* declares no language: an Arabic pattern cannot match English text. A language
|
|
475
|
+
* is added as a set of its own.
|
|
476
|
+
*
|
|
477
|
+
* Arabic attaches the conjunction, prepositions and article to the word, so its
|
|
478
|
+
* patterns match stems unanchored. The exceptions are قوة (force) and طاقة
|
|
479
|
+
* (energy), bounded the way `\bforce\b` is: unbounded, بطاقة (card) and بقوة
|
|
480
|
+
* (strongly) would match. "compound" has no Arabic entry, since مركب is also a
|
|
481
|
+
* vehicle; كيميائي covers a chemical compound. The Arabic set is pending the
|
|
482
|
+
* native review (PIE-1364).
|
|
483
|
+
*/
|
|
484
|
+
const SCIENCE_KEYWORDS = {
|
|
485
|
+
en: [
|
|
408
486
|
/chemistry|chemical|molecule|compound|periodic\s+table/i,
|
|
409
487
|
/\bchemical\s+element\b|\belement\s+symbol\b/i,
|
|
410
488
|
/biology|organism|\bDNA\b|\bRNA\b|protein|photosynthesis|ecosystem/i,
|
|
411
489
|
/physics|\bforce\b|\benergy\b|velocity|acceleration|momentum/i,
|
|
412
|
-
]
|
|
413
|
-
|
|
414
|
-
|
|
490
|
+
],
|
|
491
|
+
ar: [
|
|
492
|
+
/كيمياء|كيميائي|جزيء|جزيئ|جدول\s+(?:ال)?دوري/u,
|
|
493
|
+
/رمز\s+(?:ال)?عنصر/u,
|
|
494
|
+
/علم\s+الأحياء|كائن(?:ات)?\s+(?:ال)?حي|حمض\s+(?:ال)?نووي|بروتين|(?:بناء|تمثيل)\s+(?:ال)?ضوئي|(?:نظام|نظم|أنظمة)\s+(?:ال)?بيئي/u,
|
|
495
|
+
/فيزياء|فيزيائي|(?<!\p{L})(?:و|ال|وال)?(?:قوة|قوى|طاقة)(?!\p{L})|سرعة\s+(?:ال)?متجهة|تسارع|كمية\s+(?:ال)?حركة/u,
|
|
496
|
+
],
|
|
497
|
+
};
|