@pie-players/pie-assessment-toolkit 0.3.64 → 0.3.66
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 +81 -33
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +9 -8
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-8jgdz50p.js +51 -0
- package/dist/components/chunks/ItemToolBar-cvs646j3.js +36 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.js +10 -5
- package/dist/policy/core/PolicySource.d.ts +2 -2
- package/dist/policy/core/PolicySource.js +2 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +7 -3
- package/dist/policy/core/ToolPolicyEngine.js +11 -3
- package/dist/policy/core/compose-decision.d.ts +3 -2
- package/dist/policy/core/compose-decision.js +3 -2
- package/dist/policy/core/decision-types.d.ts +2 -2
- package/dist/policy/core/decision-types.js +2 -2
- package/dist/policy/core/feature-decision.d.ts +28 -1
- package/dist/policy/core/feature-decision.js +25 -3
- package/dist/policy/core/pnp-policy-inputs.d.ts +2 -2
- package/dist/policy/core/pnp-policy-inputs.js +2 -2
- package/dist/policy/core/provenance.d.ts +4 -1
- package/dist/policy/core/provenance.js +4 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +2 -1
- package/dist/policy/sources/PnpPolicySource.js +2 -1
- package/dist/runtime/composition-emit-scheduler.d.ts +78 -0
- package/dist/runtime/composition-emit-scheduler.js +154 -0
- package/dist/runtime/core/engine-resolver.d.ts +1 -1
- package/dist/runtime/core/engine-transition.js +3 -1
- package/dist/services/AccessibilityCatalogResolver.d.ts +49 -10
- package/dist/services/AccessibilityCatalogResolver.js +180 -11
- package/dist/services/TTSService.d.ts +11 -0
- package/dist/services/TTSService.js +220 -32
- package/dist/services/ToolRegistry.d.ts +259 -8
- package/dist/services/ToolRegistry.js +181 -11
- package/dist/services/ToolkitCoordinator.d.ts +20 -2
- package/dist/services/ToolkitCoordinator.js +47 -8
- package/dist/services/catalog-owner.d.ts +71 -0
- package/dist/services/catalog-owner.js +64 -0
- package/dist/services/createDefaultToolRegistry.d.ts +25 -58
- package/dist/services/createDefaultToolRegistry.js +24 -104
- package/dist/services/defaultPersonalNeedsProfile.d.ts +16 -14
- package/dist/services/defaultPersonalNeedsProfile.js +17 -38
- package/dist/services/framework-error.d.ts +1 -1
- package/dist/services/interfaces.d.ts +15 -3
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/tool-config-defaults.d.ts +7 -23
- package/dist/services/tool-config-defaults.js +7 -46
- package/dist/services/tool-config-validation.d.ts +1 -1
- package/dist/services/tool-config-validation.js +44 -4
- package/dist/services/tts/browser-provider.js +191 -24
- package/dist/services/tts-runtime-config.js +7 -2
- package/dist/tools/internal.d.ts +35 -0
- package/dist/tools/internal.js +33 -0
- package/dist/tools/tool-tag-map.d.ts +15 -3
- package/dist/tools/tool-tag-map.js +21 -18
- package/package.json +14 -10
- package/dist/components/chunks/ItemToolBar-3cppre9r.js +0 -51
- package/dist/components/chunks/ItemToolBar-7rq2gj8b.js +0 -22
- package/dist/runtime/catalog-registration.d.ts +0 -67
- package/dist/runtime/catalog-registration.js +0 -86
- package/dist/services/sign-language-cards.d.ts +0 -82
- package/dist/services/sign-language-cards.js +0 -133
- package/dist/tools/registrations/accessibility-tools.d.ts +0 -34
- package/dist/tools/registrations/accessibility-tools.js +0 -217
- package/dist/tools/registrations/calculator.d.ts +0 -20
- package/dist/tools/registrations/calculator.js +0 -228
- package/dist/tools/registrations/interaction-tools.d.ts +0 -27
- package/dist/tools/registrations/interaction-tools.js +0 -143
- package/dist/tools/registrations/measurement-tools.d.ts +0 -24
- package/dist/tools/registrations/measurement-tools.js +0 -130
- package/dist/tools/registrations/subject-specific-tools.d.ts +0 -27
- package/dist/tools/registrations/subject-specific-tools.js +0 -158
- package/dist/tools/registrations/tts.d.ts +0 -21
- package/dist/tools/registrations/tts.js +0 -184
|
@@ -1,46 +1,25 @@
|
|
|
1
|
-
import { createPackagedToolRegistry } from "./createDefaultToolRegistry.js";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* An empty personal-needs profile: nothing granted, nothing prohibited, nothing
|
|
3
|
+
* activated at init.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
* tool's `pnpSupportIds`, which
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* The core ships no populated default on purpose. It once derived one from every
|
|
6
|
+
* registered tool's `pnpSupportIds`, which read *registry membership* as
|
|
7
|
+
* *eligibility tier* — registration means "policy-addressable", not "universal,
|
|
8
|
+
* on by default" — so an accommodation-tier capability was granted to every
|
|
9
|
+
* student of every host that supplied no profile. The remedy at the time was a
|
|
10
|
+
* compile-time list of ids to exclude, which a host could not extend for its own
|
|
11
|
+
* accommodation.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Which capabilities a deployment grants by default is a property of the
|
|
14
|
+
* program, not of this package: TTS is a universal feature in one program and a
|
|
15
|
+
* documented accommodation in another. That belongs in policy configuration
|
|
16
|
+
* alongside the district and test-administration levels. A named preset of
|
|
17
|
+
* today's universal set ships as data from
|
|
18
|
+
* `@pie-players/pie-default-tool-loaders`, for hosts that want it.
|
|
16
19
|
*/
|
|
17
|
-
export
|
|
18
|
-
// QTI 3.0 / AfA `signLanguage`. Signed alternates are rendered by
|
|
19
|
-
// section-player's per-item media region, gated on this id.
|
|
20
|
-
"signLanguage",
|
|
21
|
-
];
|
|
22
|
-
function computeDefaultSupports() {
|
|
23
|
-
const registry = createPackagedToolRegistry();
|
|
24
|
-
const supports = new Set();
|
|
25
|
-
const excluded = new Set(ACCOMMODATION_ONLY_SUPPORT_IDS);
|
|
26
|
-
for (const tool of registry.getAllTools()) {
|
|
27
|
-
for (const supportId of tool.pnpSupportIds || []) {
|
|
28
|
-
if (excluded.has(supportId))
|
|
29
|
-
continue;
|
|
30
|
-
supports.add(supportId);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return [...supports].sort();
|
|
34
|
-
}
|
|
35
|
-
const DEFAULT_SUPPORTS = computeDefaultSupports();
|
|
36
|
-
export const DEFAULT_PERSONAL_NEEDS_PROFILE = {
|
|
37
|
-
supports: [...DEFAULT_SUPPORTS],
|
|
38
|
-
prohibitedSupports: [],
|
|
39
|
-
activateAtInit: [],
|
|
40
|
-
};
|
|
41
|
-
export function createDefaultPersonalNeedsProfile() {
|
|
20
|
+
export function createEmptyPersonalNeedsProfile() {
|
|
42
21
|
return {
|
|
43
|
-
supports: [
|
|
22
|
+
supports: [],
|
|
44
23
|
prohibitedSupports: [],
|
|
45
24
|
activateAtInit: [],
|
|
46
25
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ToolConfigDiagnostic } from "./tool-config-validation.js";
|
|
2
|
-
export type FrameworkErrorKind = "tool-config" | "runtime-init" | "runtime-dispose" | "coordinator-init" | "provider-init" | "provider-register" | "tts-init" | "tool-state-load" | "tool-state-save" | "section-controller-init" | "section-controller-dispose" | "unknown";
|
|
2
|
+
export type FrameworkErrorKind = "tool-config" | "runtime-init" | "runtime-dispose" | "coordinator-init" | "provider-init" | "provider-register" | "tts-init" | "tool-state-load" | "tool-state-save" | "section-controller-init" | "section-controller-dispose" | "tool-surface" | "unknown";
|
|
3
3
|
export type FrameworkErrorSeverity = "warning" | "error";
|
|
4
4
|
export interface FrameworkErrorModel {
|
|
5
5
|
kind: FrameworkErrorKind;
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* Part of PIE Assessment Toolkit.
|
|
9
9
|
*/
|
|
10
10
|
import type { I18nServiceApi } from "@pie-players/pie-players-shared/i18n";
|
|
11
|
-
import type {
|
|
11
|
+
import type { AccessibilityCatalog } from "@pie-players/pie-players-shared/types";
|
|
12
|
+
import type { AccessibilityCatalogResolver, CatalogChangeListener, CatalogLookupContext, CatalogLookupOptions, CatalogOwnerRegistration, CatalogOwnerView, CatalogStatistics, CatalogType, ResolvedCatalog } from "./AccessibilityCatalogResolver.js";
|
|
13
|
+
import type { CatalogOwnerContext } from "./catalog-owner.js";
|
|
12
14
|
import type { FrameworkErrorListener } from "./framework-error-bus.js";
|
|
15
|
+
import type { FrameworkErrorModel } from "./framework-error.js";
|
|
13
16
|
import type { HighlightColor, HighlightType } from "./HighlightCoordinator.js";
|
|
14
17
|
import type { SectionControllerHandle, SectionItemEventSubscriptionArgs, SectionScopedEventSubscriptionArgs, SectionEventSubscriptionArgs, ToolkitCoordinatorHooks, ToolkitInitStatus } from "./ToolkitCoordinator.js";
|
|
15
18
|
import type { ThemeConfig } from "./ThemeProvider.js";
|
|
@@ -291,11 +294,15 @@ export interface AccessibilityCatalogResolverApi {
|
|
|
291
294
|
/**
|
|
292
295
|
* Add item-level catalogs (called when rendering a new item)
|
|
293
296
|
*/
|
|
294
|
-
addItemCatalogs(catalogs:
|
|
297
|
+
addItemCatalogs(catalogs: AccessibilityCatalog[]): void;
|
|
295
298
|
/**
|
|
296
299
|
* Register catalogs scoped to a mounted content owner.
|
|
297
300
|
*/
|
|
298
|
-
registerCatalogs
|
|
301
|
+
registerCatalogs(context: CatalogOwnerContext, catalogs: AccessibilityCatalog[]): () => void;
|
|
302
|
+
/** Register every catalog carried by one mounted item or passage. */
|
|
303
|
+
registerOwner(registration: CatalogOwnerRegistration): () => void;
|
|
304
|
+
/** Bind read and change observation to one catalog owner. */
|
|
305
|
+
forOwner(context: CatalogOwnerContext): CatalogOwnerView;
|
|
299
306
|
/**
|
|
300
307
|
* Clear item-level catalogs (called when leaving an item)
|
|
301
308
|
*/
|
|
@@ -586,6 +593,11 @@ export interface ToolkitCoordinatorApi {
|
|
|
586
593
|
* `<pie-assessment-toolkit>` consume the same bus.
|
|
587
594
|
*/
|
|
588
595
|
subscribeFrameworkErrors(listener: FrameworkErrorListener): () => void;
|
|
596
|
+
/**
|
|
597
|
+
* Report a pre-built framework warning or error through the canonical bus.
|
|
598
|
+
* Optional so structural host coordinators remain assignable.
|
|
599
|
+
*/
|
|
600
|
+
reportFrameworkError?(model: FrameworkErrorModel): void;
|
|
589
601
|
/**
|
|
590
602
|
* Resolve the visible tool set for a given placement level + scope.
|
|
591
603
|
* Returns the engine's full decision (visible tools, diagnostics,
|
|
@@ -167,7 +167,7 @@ export declare const QTI_STANDARD_ACCESS_FEATURES: {
|
|
|
167
167
|
/**
|
|
168
168
|
* Flat list of all standard access features for validation
|
|
169
169
|
*/
|
|
170
|
-
export declare const ALL_STANDARD_ACCESS_FEATURES: ("transcript" | "braille" | "textToSpeech" | "
|
|
170
|
+
export declare const ALL_STANDARD_ACCESS_FEATURES: ("transcript" | "braille" | "textToSpeech" | "calculator" | "strikethrough" | "answerEliminator" | "magnification" | "screenMagnifier" | "zoomable" | "highContrastDisplay" | "highContrastAudio" | "colorContrast" | "invertColors" | "displayTransformability" | "largePrint" | "fontEnlargement" | "resizeText" | "alternativeText" | "longDescription" | "describedMath" | "tactileGraphic" | "tactileObject" | "audioDescription" | "readAloud" | "humanVoice" | "syntheticVoice" | "speechRate" | "speechVolume" | "voicePitch" | "captions" | "closedCaptions" | "openCaptions" | "signLanguage" | "subtitles" | "audioControl" | "noBackgroundAudio" | "keyboardControl" | "mouseControl" | "touchControl" | "voiceControl" | "switchControl" | "eyeGazeControl" | "singleSwitchAccess" | "stickyKeys" | "keyboardShortcuts" | "timingControl" | "unlimitedTime" | "extendedTime" | "pauseControl" | "simplifiedLanguage" | "reducedComplexity" | "structuralNavigation" | "tableOfContents" | "reducedDistraction" | "noFlashing" | "pauseAnimation" | "annotations" | "bookmarking" | "highlighting" | "guidedNavigation" | "thesaurus" | "spellingAssistance" | "grammarAssistance" | "lineSpacing" | "wordSpacing" | "letterSpacing" | "fontFamily" | "readingMask" | "readingGuide" | "readingRuler" | "wordHighlighting" | "lineHighlighting" | "focusIndicator" | "printableResource" | "nemeth" | "refreshableBraille" | "index" | "pageNavigation" | "skipContent" | "breadcrumbs" | "searchable" | "fullTextSearch" | "multilingualText" | "translatedText" | "glossary" | "signLanguageInterpretation" | "visualLanguage" | "protractor" | "ruler" | "graph" | "graphingCalculator" | "periodicTable" | "formulaSheet" | "answerMasking" | "itemGlossary" | "tutorialAvailable")[];
|
|
171
171
|
/**
|
|
172
172
|
* Example PNP configurations for common accessibility needs
|
|
173
173
|
* These are NOT official profiles but illustrative examples showing
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placement defaults this package can hold.
|
|
3
|
+
*
|
|
4
|
+
* Only the empty one. `PACKAGED_TOOL_PLACEMENT` and
|
|
5
|
+
* `SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT` name capabilities, so they moved to
|
|
6
|
+
* the composition layer (`@pie-players/pie-default-tool-loaders`).
|
|
7
|
+
*/
|
|
1
8
|
export declare const DEFAULT_TOOL_PLACEMENT: {
|
|
2
9
|
readonly assessment: readonly [];
|
|
3
10
|
readonly section: readonly [];
|
|
@@ -6,26 +13,3 @@ export declare const DEFAULT_TOOL_PLACEMENT: {
|
|
|
6
13
|
readonly rubric: readonly [];
|
|
7
14
|
readonly element: readonly [];
|
|
8
15
|
};
|
|
9
|
-
/**
|
|
10
|
-
* Complete placement preset for hosts that want to enable all packaged tools.
|
|
11
|
-
* Prefer explicit host configuration over implicit defaults.
|
|
12
|
-
*/
|
|
13
|
-
export declare const PACKAGED_TOOL_PLACEMENT: {
|
|
14
|
-
readonly assessment: readonly ["theme"];
|
|
15
|
-
readonly section: readonly ["theme"];
|
|
16
|
-
readonly item: readonly ["textToSpeech", "highlighter", "annotationToolbar", "graph", "periodicTable"];
|
|
17
|
-
readonly passage: readonly ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"];
|
|
18
|
-
readonly rubric: readonly ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"];
|
|
19
|
-
readonly element: readonly ["calculator", "answerEliminator", "textToSpeech", "ruler", "protractor", "highlighter", "annotationToolbar", "graph", "periodicTable"];
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* Opt-in section-player placement preset for hosts that want to expose every
|
|
23
|
-
* packaged tool once at its normal assessment surface. This is not exhaustive:
|
|
24
|
-
* `supportedLevels` still defines where tools can run, and hosts can still
|
|
25
|
-
* choose different placement for custom UX.
|
|
26
|
-
*/
|
|
27
|
-
export declare const SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT: {
|
|
28
|
-
section: string[];
|
|
29
|
-
item: string[];
|
|
30
|
-
passage: string[];
|
|
31
|
-
};
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placement defaults this package can hold.
|
|
3
|
+
*
|
|
4
|
+
* Only the empty one. `PACKAGED_TOOL_PLACEMENT` and
|
|
5
|
+
* `SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT` name capabilities, so they moved to
|
|
6
|
+
* the composition layer (`@pie-players/pie-default-tool-loaders`).
|
|
7
|
+
*/
|
|
1
8
|
export const DEFAULT_TOOL_PLACEMENT = {
|
|
2
9
|
assessment: [],
|
|
3
10
|
section: [],
|
|
@@ -6,49 +13,3 @@ export const DEFAULT_TOOL_PLACEMENT = {
|
|
|
6
13
|
rubric: [],
|
|
7
14
|
element: [],
|
|
8
15
|
};
|
|
9
|
-
/**
|
|
10
|
-
* Complete placement preset for hosts that want to enable all packaged tools.
|
|
11
|
-
* Prefer explicit host configuration over implicit defaults.
|
|
12
|
-
*/
|
|
13
|
-
export const PACKAGED_TOOL_PLACEMENT = {
|
|
14
|
-
assessment: ["theme"],
|
|
15
|
-
section: ["theme"],
|
|
16
|
-
item: [
|
|
17
|
-
"textToSpeech",
|
|
18
|
-
"highlighter",
|
|
19
|
-
"annotationToolbar",
|
|
20
|
-
"graph",
|
|
21
|
-
"periodicTable",
|
|
22
|
-
],
|
|
23
|
-
passage: ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"],
|
|
24
|
-
rubric: ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"],
|
|
25
|
-
element: [
|
|
26
|
-
"calculator",
|
|
27
|
-
"answerEliminator",
|
|
28
|
-
"textToSpeech",
|
|
29
|
-
"ruler",
|
|
30
|
-
"protractor",
|
|
31
|
-
"highlighter",
|
|
32
|
-
"annotationToolbar",
|
|
33
|
-
"graph",
|
|
34
|
-
"periodicTable",
|
|
35
|
-
],
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* Opt-in section-player placement preset for hosts that want to expose every
|
|
39
|
-
* packaged tool once at its normal assessment surface. This is not exhaustive:
|
|
40
|
-
* `supportedLevels` still defines where tools can run, and hosts can still
|
|
41
|
-
* choose different placement for custom UX.
|
|
42
|
-
*/
|
|
43
|
-
export const SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT = {
|
|
44
|
-
section: [
|
|
45
|
-
"theme",
|
|
46
|
-
"graph",
|
|
47
|
-
"periodicTable",
|
|
48
|
-
"lineReader",
|
|
49
|
-
"ruler",
|
|
50
|
-
"protractor",
|
|
51
|
-
],
|
|
52
|
-
item: ["calculator", "textToSpeech", "answerEliminator", "annotationToolbar"],
|
|
53
|
-
passage: ["textToSpeech", "annotationToolbar"],
|
|
54
|
-
};
|
|
@@ -4,7 +4,7 @@ import type { ToolRegistry } from "./ToolRegistry.js";
|
|
|
4
4
|
export type ToolConfigStrictness = "off" | "warn" | "error";
|
|
5
5
|
export type ToolConfigDiagnosticSeverity = "warning" | "error";
|
|
6
6
|
export interface ToolConfigDiagnostic {
|
|
7
|
-
code: "tools.unknownToolId" | "tools.unsupportedLevel" | "tools.unknownProviderKey" | "tools.removedProviderKey" | "tools.providerSanitizeFailed" | "tools.providerValidateFailed" | "tools.invalidProviderValidation";
|
|
7
|
+
code: "tools.unknownToolId" | "tools.unsupportedLevel" | "tools.unplaceableActivation" | "tools.registryUnavailable" | "tools.unknownProviderKey" | "tools.removedProviderKey" | "tools.providerSanitizeFailed" | "tools.providerValidateFailed" | "tools.invalidProviderValidation";
|
|
8
8
|
severity: ToolConfigDiagnosticSeverity;
|
|
9
9
|
path: string;
|
|
10
10
|
message: string;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { normalizeToolsConfig, } from "./tools-config-normalizer.js";
|
|
2
2
|
import { frameworkErrorFromToolConfigDiagnostics, frameworkErrorFromUnknown, } from "./framework-error.js";
|
|
3
|
-
import { createPackagedToolRegistry } from "./createDefaultToolRegistry.js";
|
|
4
3
|
export function frameworkErrorFromToolConfigValidation(args) {
|
|
5
4
|
if (Array.isArray(args.diagnostics) && args.diagnostics.length > 0) {
|
|
6
5
|
return frameworkErrorFromToolConfigDiagnostics({
|
|
@@ -120,6 +119,20 @@ function collectPlacementDiagnostics(config, toolMap, diagnostics) {
|
|
|
120
119
|
}));
|
|
121
120
|
continue;
|
|
122
121
|
}
|
|
122
|
+
if (tool.activation === "region") {
|
|
123
|
+
// A region capability has no toolbar button, so placing it names a
|
|
124
|
+
// surface that will never render it. Reported at the placement rather
|
|
125
|
+
// than at render time, where the symptom is an absent accommodation and
|
|
126
|
+
// no error.
|
|
127
|
+
diagnostics.push(createDiagnostic({
|
|
128
|
+
code: "tools.unplaceableActivation",
|
|
129
|
+
severity: "error",
|
|
130
|
+
path: `placement.${level}`,
|
|
131
|
+
message: `Tool "${toolId}" renders into a host surface (activation "region") and cannot be placed on the ${level} toolbar. Remove it from placement.${level}; its availability comes from policy plus its content dependency.`,
|
|
132
|
+
toolId,
|
|
133
|
+
}));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
123
136
|
const supportsLevel = tool.supportedLevels.includes(level);
|
|
124
137
|
// Leniency policy: section-capable tools are tolerated in item placement.
|
|
125
138
|
// This avoids strict failures for common host config patterns while still
|
|
@@ -204,9 +217,27 @@ function throwValidationError(diagnostics, source) {
|
|
|
204
217
|
export function normalizeAndValidateToolsConfig(input, options = {}) {
|
|
205
218
|
const strictness = normalizeToolConfigStrictness(options.strictness);
|
|
206
219
|
const source = options.source ?? "tools";
|
|
207
|
-
const registryTools = getRegistryToolMap(options.toolRegistry
|
|
220
|
+
const registryTools = getRegistryToolMap(options.toolRegistry);
|
|
208
221
|
const normalized = normalizeToolsConfig(input);
|
|
209
222
|
const diagnostics = [];
|
|
223
|
+
// Tool-id and provider checks need a registry to check against, and every
|
|
224
|
+
// collector below returns early without one. Say so: this package no longer
|
|
225
|
+
// falls back to a packaged registry, so a caller that used to get id
|
|
226
|
+
// validation for free now gets none, and a silent downgrade from "your ids are
|
|
227
|
+
// valid" to "nobody looked" is the kind of change that surfaces as a typo
|
|
228
|
+
// reaching a learner.
|
|
229
|
+
const hasConfiguredTools = Object.values(normalized.placement).some((ids) => ids.length > 0) ||
|
|
230
|
+
Object.keys(normalized.providers).length > 0 ||
|
|
231
|
+
(normalized.policy.allowed?.length ?? 0) > 0 ||
|
|
232
|
+
(normalized.policy.blocked?.length ?? 0) > 0;
|
|
233
|
+
if (registryTools.size === 0 && hasConfiguredTools) {
|
|
234
|
+
diagnostics.push(createDiagnostic({
|
|
235
|
+
code: "tools.registryUnavailable",
|
|
236
|
+
severity: "warning",
|
|
237
|
+
path: "tools",
|
|
238
|
+
message: 'No tool registry was supplied, so tool ids, placement levels and provider config were not validated. Pass `toolRegistry` — for the packaged capability set, `createPackagedToolRegistry()` from "@pie-players/pie-default-tool-loaders".',
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
210
241
|
const hasRemovedTtsKey = Object.hasOwn(normalized.providers, "tts");
|
|
211
242
|
const nextProviders = {
|
|
212
243
|
...(normalized.providers || {}),
|
|
@@ -233,8 +264,17 @@ export function normalizeAndValidateToolsConfig(input, options = {}) {
|
|
|
233
264
|
if (strictness === "warn") {
|
|
234
265
|
emitWarnings(diagnostics, source);
|
|
235
266
|
}
|
|
236
|
-
|
|
237
|
-
|
|
267
|
+
// Severity decides what `strictness: "error"` rejects. Every diagnostic this
|
|
268
|
+
// function raised was `"error"` until `tools.registryUnavailable`, which
|
|
269
|
+
// reports that validation could not run rather than that the config is wrong —
|
|
270
|
+
// throwing on it would turn "no registry supplied" from a host's existing,
|
|
271
|
+
// working setup into a construction failure.
|
|
272
|
+
const blocking = diagnostics.filter((entry) => entry.severity === "error");
|
|
273
|
+
if (strictness === "error" && blocking.length > 0) {
|
|
274
|
+
throwValidationError(blocking, source);
|
|
275
|
+
}
|
|
276
|
+
if (strictness === "error" && blocking.length === 0) {
|
|
277
|
+
emitWarnings(diagnostics, source);
|
|
238
278
|
}
|
|
239
279
|
return {
|
|
240
280
|
config: nextConfig,
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* Part of PIE Assessment Toolkit.
|
|
8
8
|
*/
|
|
9
9
|
import { segmentSentences as segmentTextToSentences } from "./text-segmentation.js";
|
|
10
|
+
const NATIVE_START_TIMEOUT_MS = 5_000;
|
|
11
|
+
const VOICE_INVENTORY_TIMEOUT_MS = 2_000;
|
|
10
12
|
const normalizeLanguageCode = (value) => String(value || "")
|
|
11
13
|
.trim()
|
|
12
14
|
.toLowerCase();
|
|
@@ -16,17 +18,18 @@ const browserLanguage = () => {
|
|
|
16
18
|
: "";
|
|
17
19
|
return normalizeLanguageCode(navigatorLanguage || "en-US");
|
|
18
20
|
};
|
|
19
|
-
const findBrowserVoice = (voices,
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const findBrowserVoice = (voices, preferredVoice) => {
|
|
22
|
+
if (preferredVoice) {
|
|
23
|
+
return (voices.find((voice) => voice.voiceURI === preferredVoice) ||
|
|
24
|
+
voices.find((voice) => voice.name === preferredVoice) ||
|
|
25
|
+
null);
|
|
24
26
|
}
|
|
25
27
|
const language = browserLanguage();
|
|
26
28
|
const languagePrefix = language.split("-")[0] || "en";
|
|
27
29
|
const matchesLanguage = (voice) => {
|
|
28
30
|
const voiceLanguage = normalizeLanguageCode(voice.lang);
|
|
29
|
-
return voiceLanguage === language ||
|
|
31
|
+
return (voiceLanguage === language ||
|
|
32
|
+
voiceLanguage.startsWith(`${languagePrefix}-`));
|
|
30
33
|
};
|
|
31
34
|
const ranked = [
|
|
32
35
|
(voice) => voice.localService && matchesLanguage(voice),
|
|
@@ -94,9 +97,98 @@ class BrowserTTSProviderImpl {
|
|
|
94
97
|
_isPlaying = false;
|
|
95
98
|
_isPaused = false;
|
|
96
99
|
speakRunId = 0;
|
|
100
|
+
settlePendingVoiceWait = null;
|
|
101
|
+
settlePendingChunk = null;
|
|
102
|
+
onPlaybackStart = undefined;
|
|
97
103
|
constructor(config) {
|
|
98
104
|
this.config = config;
|
|
99
105
|
}
|
|
106
|
+
waitForBrowserVoices(runId, configuredVoice) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const synth = speechSynthesis;
|
|
109
|
+
const canUseEventTarget = typeof synth.addEventListener === "function" &&
|
|
110
|
+
typeof synth.removeEventListener === "function";
|
|
111
|
+
const previousHandler = canUseEventTarget ? null : synth.onvoiceschanged;
|
|
112
|
+
let settled = false;
|
|
113
|
+
let timeout = null;
|
|
114
|
+
let pendingSettlement = null;
|
|
115
|
+
const cleanup = () => {
|
|
116
|
+
if (timeout !== null) {
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
timeout = null;
|
|
119
|
+
}
|
|
120
|
+
if (canUseEventTarget) {
|
|
121
|
+
synth.removeEventListener("voiceschanged", onVoicesChanged);
|
|
122
|
+
}
|
|
123
|
+
else if (synth.onvoiceschanged === propertyHandler) {
|
|
124
|
+
synth.onvoiceschanged = previousHandler;
|
|
125
|
+
}
|
|
126
|
+
if (pendingSettlement &&
|
|
127
|
+
this.settlePendingVoiceWait === pendingSettlement) {
|
|
128
|
+
this.settlePendingVoiceWait = null;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const finish = (voicesAvailable, error) => {
|
|
132
|
+
if (settled)
|
|
133
|
+
return;
|
|
134
|
+
settled = true;
|
|
135
|
+
cleanup();
|
|
136
|
+
if (error) {
|
|
137
|
+
reject(error);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
resolve(voicesAvailable);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const inspectVoiceInventory = () => {
|
|
144
|
+
if (runId !== this.speakRunId) {
|
|
145
|
+
finish(false);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (synth.getVoices().length > 0)
|
|
149
|
+
finish(true);
|
|
150
|
+
};
|
|
151
|
+
const onVoicesChanged = () => inspectVoiceInventory();
|
|
152
|
+
const propertyHandler = (event) => {
|
|
153
|
+
try {
|
|
154
|
+
previousHandler?.call(synth, event);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
inspectVoiceInventory();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
pendingSettlement = () => finish(false);
|
|
161
|
+
this.settlePendingVoiceWait = pendingSettlement;
|
|
162
|
+
timeout = setTimeout(() => finish(false, new Error(`Configured browser voice "${configuredVoice}" could not be resolved because the browser did not publish its voice inventory within ${VOICE_INVENTORY_TIMEOUT_MS / 1_000} seconds.`)), VOICE_INVENTORY_TIMEOUT_MS);
|
|
163
|
+
if (canUseEventTarget) {
|
|
164
|
+
synth.addEventListener("voiceschanged", onVoicesChanged);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
synth.onvoiceschanged = propertyHandler;
|
|
168
|
+
}
|
|
169
|
+
// Close the getVoices()/listener-registration race without polling.
|
|
170
|
+
inspectVoiceInventory();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async resolveBrowserVoice(runId) {
|
|
174
|
+
const configuredVoice = typeof this.config?.voice === "string" ? this.config.voice.trim() : "";
|
|
175
|
+
let voices = speechSynthesis.getVoices();
|
|
176
|
+
if (configuredVoice && voices.length === 0) {
|
|
177
|
+
const voicesAvailable = await this.waitForBrowserVoices(runId, configuredVoice);
|
|
178
|
+
if (!voicesAvailable || runId !== this.speakRunId) {
|
|
179
|
+
return { shouldContinue: false, voice: null };
|
|
180
|
+
}
|
|
181
|
+
voices = speechSynthesis.getVoices();
|
|
182
|
+
}
|
|
183
|
+
if (runId !== this.speakRunId) {
|
|
184
|
+
return { shouldContinue: false, voice: null };
|
|
185
|
+
}
|
|
186
|
+
const voice = findBrowserVoice(voices, configuredVoice || undefined);
|
|
187
|
+
if (configuredVoice && !voice) {
|
|
188
|
+
throw new Error(`Configured browser voice "${configuredVoice}" is unavailable. Select a voice exposed by this browser using its voiceURI or name.`);
|
|
189
|
+
}
|
|
190
|
+
return { shouldContinue: true, voice };
|
|
191
|
+
}
|
|
100
192
|
async speak(text) {
|
|
101
193
|
if (!this.config) {
|
|
102
194
|
throw new Error("TTS not initialized");
|
|
@@ -104,12 +196,15 @@ class BrowserTTSProviderImpl {
|
|
|
104
196
|
// Invalidate any in-flight run and cancel current utterance.
|
|
105
197
|
this.stop();
|
|
106
198
|
const runId = this.speakRunId;
|
|
199
|
+
const voiceResolution = await this.resolveBrowserVoice(runId);
|
|
200
|
+
if (!voiceResolution.shouldContinue)
|
|
201
|
+
return;
|
|
107
202
|
const chunks = this.splitIntoChunks(text);
|
|
108
203
|
for (const chunk of chunks) {
|
|
109
204
|
if (runId !== this.speakRunId) {
|
|
110
205
|
break;
|
|
111
206
|
}
|
|
112
|
-
const shouldContinue = await this.speakChunk(chunk.text, chunk.offset, runId);
|
|
207
|
+
const shouldContinue = await this.speakChunk(chunk.text, chunk.offset, runId, voiceResolution.voice);
|
|
113
208
|
if (!shouldContinue) {
|
|
114
209
|
break;
|
|
115
210
|
}
|
|
@@ -121,6 +216,9 @@ class BrowserTTSProviderImpl {
|
|
|
121
216
|
}
|
|
122
217
|
this.stop();
|
|
123
218
|
const runId = this.speakRunId;
|
|
219
|
+
const voiceResolution = await this.resolveBrowserVoice(runId);
|
|
220
|
+
if (!voiceResolution.shouldContinue)
|
|
221
|
+
return;
|
|
124
222
|
for (const segment of segments) {
|
|
125
223
|
if (runId !== this.speakRunId)
|
|
126
224
|
break;
|
|
@@ -128,7 +226,7 @@ class BrowserTTSProviderImpl {
|
|
|
128
226
|
for (const chunk of chunks) {
|
|
129
227
|
if (runId !== this.speakRunId)
|
|
130
228
|
break;
|
|
131
|
-
const shouldContinue = await this.speakChunk(chunk.text, segment.startOffset + chunk.offset, runId);
|
|
229
|
+
const shouldContinue = await this.speakChunk(chunk.text, segment.startOffset + chunk.offset, runId, voiceResolution.voice);
|
|
132
230
|
if (!shouldContinue)
|
|
133
231
|
break;
|
|
134
232
|
}
|
|
@@ -213,7 +311,7 @@ class BrowserTTSProviderImpl {
|
|
|
213
311
|
const match = slice.match(/^\s*([^\s]+)/);
|
|
214
312
|
return match?.[1]?.length || 1;
|
|
215
313
|
}
|
|
216
|
-
async speakChunk(chunkText, chunkOffset, runId) {
|
|
314
|
+
async speakChunk(chunkText, chunkOffset, runId, voice) {
|
|
217
315
|
return new Promise((resolve, reject) => {
|
|
218
316
|
if (runId !== this.speakRunId) {
|
|
219
317
|
resolve(false);
|
|
@@ -222,47 +320,99 @@ class BrowserTTSProviderImpl {
|
|
|
222
320
|
const utterance = new SpeechSynthesisUtterance(chunkText);
|
|
223
321
|
this.utterance = utterance;
|
|
224
322
|
// Apply config
|
|
225
|
-
const voice = findBrowserVoice(speechSynthesis.getVoices(), this.config?.voice);
|
|
226
323
|
if (voice && shouldAssignBrowserVoice(voice))
|
|
227
324
|
utterance.voice = voice;
|
|
228
325
|
if (this.config?.rate)
|
|
229
326
|
utterance.rate = this.config.rate;
|
|
230
327
|
if (this.config?.pitch)
|
|
231
328
|
utterance.pitch = this.config.pitch;
|
|
329
|
+
let didStart = false;
|
|
330
|
+
let settled = false;
|
|
331
|
+
let startTimeout = null;
|
|
332
|
+
let pendingSettlement = null;
|
|
232
333
|
const clearOwnedUtterance = () => {
|
|
233
334
|
if (this.utterance === utterance) {
|
|
234
335
|
this.utterance = null;
|
|
235
336
|
}
|
|
236
337
|
};
|
|
338
|
+
const clearStartTimeout = () => {
|
|
339
|
+
if (startTimeout !== null) {
|
|
340
|
+
clearTimeout(startTimeout);
|
|
341
|
+
startTimeout = null;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const detachHandlers = () => {
|
|
345
|
+
utterance.onstart = null;
|
|
346
|
+
utterance.onend = null;
|
|
347
|
+
utterance.onerror = null;
|
|
348
|
+
utterance.onpause = null;
|
|
349
|
+
utterance.onresume = null;
|
|
350
|
+
utterance.onboundary = null;
|
|
351
|
+
};
|
|
352
|
+
const settle = (shouldContinue, error) => {
|
|
353
|
+
if (settled)
|
|
354
|
+
return;
|
|
355
|
+
settled = true;
|
|
356
|
+
clearStartTimeout();
|
|
357
|
+
detachHandlers();
|
|
358
|
+
clearOwnedUtterance();
|
|
359
|
+
if (pendingSettlement &&
|
|
360
|
+
this.settlePendingChunk === pendingSettlement) {
|
|
361
|
+
this.settlePendingChunk = null;
|
|
362
|
+
}
|
|
363
|
+
if (runId === this.speakRunId) {
|
|
364
|
+
this._isPlaying = false;
|
|
365
|
+
this._isPaused = false;
|
|
366
|
+
}
|
|
367
|
+
if (error) {
|
|
368
|
+
reject(error);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
resolve(shouldContinue);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
const browserEngineStartError = (reason) => new Error(reason === "ended"
|
|
375
|
+
? "Browser speech synthesis ended before audio started. The browser speech engine may be unavailable or stuck; try another voice or restart the browser."
|
|
376
|
+
: `Browser speech synthesis did not start within ${NATIVE_START_TIMEOUT_MS / 1_000} seconds. The browser speech engine may be unavailable or stuck; try another voice or restart the browser.`);
|
|
377
|
+
pendingSettlement = () => settle(false);
|
|
378
|
+
this.settlePendingChunk = pendingSettlement;
|
|
237
379
|
utterance.onstart = () => {
|
|
238
|
-
if (runId !== this.speakRunId)
|
|
380
|
+
if (runId !== this.speakRunId) {
|
|
381
|
+
settle(false);
|
|
239
382
|
return;
|
|
383
|
+
}
|
|
384
|
+
didStart = true;
|
|
385
|
+
clearStartTimeout();
|
|
240
386
|
this._isPlaying = true;
|
|
241
387
|
this._isPaused = false;
|
|
388
|
+
try {
|
|
389
|
+
this.onPlaybackStart?.();
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
console.error("Browser TTS playback-start callback failed", error);
|
|
393
|
+
}
|
|
242
394
|
};
|
|
243
395
|
utterance.onend = () => {
|
|
244
|
-
clearOwnedUtterance();
|
|
245
396
|
if (runId !== this.speakRunId) {
|
|
246
|
-
|
|
397
|
+
settle(false);
|
|
247
398
|
return;
|
|
248
399
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
400
|
+
if (!didStart) {
|
|
401
|
+
settle(false, browserEngineStartError("ended"));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
settle(true);
|
|
252
405
|
};
|
|
253
406
|
utterance.onerror = (event) => {
|
|
254
|
-
clearOwnedUtterance();
|
|
255
407
|
if (runId !== this.speakRunId) {
|
|
256
|
-
|
|
408
|
+
settle(false);
|
|
257
409
|
return;
|
|
258
410
|
}
|
|
259
|
-
this._isPlaying = false;
|
|
260
|
-
this._isPaused = false;
|
|
261
411
|
if (event.error === "interrupted" || event.error === "canceled") {
|
|
262
|
-
|
|
412
|
+
settle(false);
|
|
263
413
|
return;
|
|
264
414
|
}
|
|
265
|
-
|
|
415
|
+
settle(false, new Error(`Speech synthesis error: ${event.error}`));
|
|
266
416
|
};
|
|
267
417
|
utterance.onpause = () => {
|
|
268
418
|
if (runId !== this.speakRunId)
|
|
@@ -299,7 +449,20 @@ class BrowserTTSProviderImpl {
|
|
|
299
449
|
console.log("[BrowserProvider] Calling onWordBoundary with word:", word, "at position:", absoluteBoundaryStart);
|
|
300
450
|
this.onWordBoundary(word, absoluteBoundaryStart, wordLength);
|
|
301
451
|
};
|
|
302
|
-
|
|
452
|
+
startTimeout = setTimeout(() => {
|
|
453
|
+
if (settled || didStart || runId !== this.speakRunId)
|
|
454
|
+
return;
|
|
455
|
+
settle(false, browserEngineStartError("timedOut"));
|
|
456
|
+
speechSynthesis.cancel();
|
|
457
|
+
}, NATIVE_START_TIMEOUT_MS);
|
|
458
|
+
try {
|
|
459
|
+
speechSynthesis.speak(utterance);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
settle(false, error instanceof Error
|
|
463
|
+
? error
|
|
464
|
+
: new Error("Browser speech synthesis failed to queue speech"));
|
|
465
|
+
}
|
|
303
466
|
});
|
|
304
467
|
}
|
|
305
468
|
pause() {
|
|
@@ -313,8 +476,12 @@ class BrowserTTSProviderImpl {
|
|
|
313
476
|
}
|
|
314
477
|
}
|
|
315
478
|
stop() {
|
|
479
|
+
const utterance = this.utterance;
|
|
480
|
+
const shouldCancel = this._isPlaying || utterance !== null;
|
|
316
481
|
this.speakRunId += 1;
|
|
317
|
-
|
|
482
|
+
this.settlePendingVoiceWait?.();
|
|
483
|
+
this.settlePendingChunk?.();
|
|
484
|
+
if (shouldCancel) {
|
|
318
485
|
speechSynthesis.cancel();
|
|
319
486
|
}
|
|
320
487
|
this.utterance = null;
|