@pie-players/pie-assessment-toolkit 0.3.63 → 0.3.64
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 +63 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +11 -11
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/{ItemToolBar-843902tp.js → ItemToolBar-3cppre9r.js} +26 -26
- package/dist/components/chunks/{ItemToolBar-84nv78dy.js → ItemToolBar-7rq2gj8b.js} +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +21 -0
- package/dist/policy/core/ToolPolicyEngine.js +27 -0
- package/dist/policy/core/feature-decision.d.ts +57 -0
- package/dist/policy/core/feature-decision.js +40 -0
- package/dist/policy/engine.d.ts +1 -0
- package/dist/policy/sources/PnpPolicySource.d.ts +22 -0
- package/dist/policy/sources/PnpPolicySource.js +41 -11
- package/dist/runtime/catalog-registration.d.ts +56 -1
- package/dist/runtime/catalog-registration.js +64 -31
- package/dist/services/AccessibilityCatalogResolver.d.ts +100 -4
- package/dist/services/AccessibilityCatalogResolver.js +183 -58
- package/dist/services/SSMLExtractor.js +28 -18
- package/dist/services/TTSService.d.ts +25 -0
- package/dist/services/TTSService.js +241 -45
- package/dist/services/ToolkitCoordinator.d.ts +23 -2
- package/dist/services/ToolkitCoordinator.js +24 -0
- package/dist/services/catalog-media.d.ts +25 -0
- package/dist/services/catalog-media.js +101 -0
- package/dist/services/defaultPersonalNeedsProfile.d.ts +16 -0
- package/dist/services/defaultPersonalNeedsProfile.js +23 -0
- package/dist/services/interfaces.d.ts +29 -2
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/sign-language-cards.d.ts +82 -0
- package/dist/services/sign-language-cards.js +133 -0
- package/dist/services/spoken-audio-cards.d.ts +54 -0
- package/dist/services/spoken-audio-cards.js +66 -0
- package/dist/services/tts/math-aware-text-processing.js +3 -3
- package/dist/services/tts/text-processing.d.ts +51 -0
- package/dist/services/tts/text-processing.js +117 -1
- package/package.json +9 -9
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import { createPackagedToolRegistry } from "./createDefaultToolRegistry.js";
|
|
2
|
+
/**
|
|
3
|
+
* PNP support ids that must never enter the computed default profile, however
|
|
4
|
+
* they reach the registry.
|
|
5
|
+
*
|
|
6
|
+
* `computeDefaultSupports()` derives the fallback profile from every registered
|
|
7
|
+
* tool's `pnpSupportIds`, which is right for universal features — a highlighter
|
|
8
|
+
* or a zoom control should be there for every student by default. It is wrong
|
|
9
|
+
* for an **accommodation**: signing requires a documented need (IEP / 504), so
|
|
10
|
+
* granting it to every host that does not supply its own profile would invert
|
|
11
|
+
* the eligibility tier. Registry membership is about being policy-addressable,
|
|
12
|
+
* not about who may enable it.
|
|
13
|
+
*
|
|
14
|
+
* Excluded here rather than by declining to register, so the guarantee holds
|
|
15
|
+
* even if a signing tool is later registered for some other reason.
|
|
16
|
+
*/
|
|
17
|
+
export const ACCOMMODATION_ONLY_SUPPORT_IDS = [
|
|
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
|
+
];
|
|
2
22
|
function computeDefaultSupports() {
|
|
3
23
|
const registry = createPackagedToolRegistry();
|
|
4
24
|
const supports = new Set();
|
|
25
|
+
const excluded = new Set(ACCOMMODATION_ONLY_SUPPORT_IDS);
|
|
5
26
|
for (const tool of registry.getAllTools()) {
|
|
6
27
|
for (const supportId of tool.pnpSupportIds || []) {
|
|
28
|
+
if (excluded.has(supportId))
|
|
29
|
+
continue;
|
|
7
30
|
supports.add(supportId);
|
|
8
31
|
}
|
|
9
32
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Part of PIE Assessment Toolkit.
|
|
9
9
|
*/
|
|
10
10
|
import type { I18nServiceApi } from "@pie-players/pie-players-shared/i18n";
|
|
11
|
-
import type { AccessibilityCatalogResolver, CatalogLookupContext, CatalogLookupOptions, CatalogOwnerContext, CatalogStatistics, CatalogType, ResolvedCatalog } from "./AccessibilityCatalogResolver.js";
|
|
11
|
+
import type { AccessibilityCatalogResolver, CatalogChangeListener, CatalogLookupContext, CatalogLookupOptions, CatalogOwnerContext, CatalogStatistics, CatalogType, ResolvedCatalog } from "./AccessibilityCatalogResolver.js";
|
|
12
12
|
import type { FrameworkErrorListener } from "./framework-error-bus.js";
|
|
13
13
|
import type { HighlightColor, HighlightType } from "./HighlightCoordinator.js";
|
|
14
14
|
import type { SectionControllerHandle, SectionItemEventSubscriptionArgs, SectionScopedEventSubscriptionArgs, SectionEventSubscriptionArgs, ToolkitCoordinatorHooks, ToolkitInitStatus } from "./ToolkitCoordinator.js";
|
|
@@ -18,7 +18,7 @@ import type { PlaybackState, TTSConfig } from "./TTSService.js";
|
|
|
18
18
|
import type { TTSHighlightTargetResolverProvider } from "./tts/highlight-target-resolver.js";
|
|
19
19
|
import type { ToolPlacementConfig, ToolPlacementLevel, ToolProviderConfig } from "./tools-config-normalizer.js";
|
|
20
20
|
import type { ToolProviderRegistry } from "./tool-providers/ToolProviderRegistry.js";
|
|
21
|
-
import type { PolicySource, PnpEnforcementMode, ResolvedEngineInputs, ToolPolicyChangeListener, ToolPolicyDecision, ToolPolicyDecisionRequest } from "../policy/engine.js";
|
|
21
|
+
import type { FeaturePolicyDecision, PolicySource, PnpEnforcementMode, ResolvedEngineInputs, ToolPolicyChangeListener, ToolPolicyDecision, ToolPolicyDecisionRequest } from "../policy/engine.js";
|
|
22
22
|
import type { AssessmentEntity, AssessmentItemRef } from "@pie-players/pie-players-shared/types";
|
|
23
23
|
import type { ITTSProvider, TTSProviderCapabilities } from "@pie-players/pie-tts";
|
|
24
24
|
import type { ResolvedToolContext, ToolContextResolver, ToolContextResolverContext, ToolContextResolverMap } from "./ToolRegistry.js";
|
|
@@ -593,6 +593,16 @@ export interface ToolkitCoordinatorApi {
|
|
|
593
593
|
* `decision.visibleTools` themselves.
|
|
594
594
|
*/
|
|
595
595
|
decideToolPolicy(request: ToolPolicyDecisionRequest): ToolPolicyDecision;
|
|
596
|
+
/**
|
|
597
|
+
* Resolve eligibility for one PNP/AfA feature id through the six-level
|
|
598
|
+
* precedence, independent of toolbar placement — for capabilities that
|
|
599
|
+
* render as their own surface rather than a toolbar button (a signed
|
|
600
|
+
* alternate's region, for example).
|
|
601
|
+
*
|
|
602
|
+
* Optional so host-supplied coordinator stubs predating this method stay
|
|
603
|
+
* assignable; call sites must feature-detect.
|
|
604
|
+
*/
|
|
605
|
+
decideFeaturePolicy?(featureId: string): FeaturePolicyDecision;
|
|
596
606
|
/**
|
|
597
607
|
* Subscribe to policy-engine change events. Fires whenever the
|
|
598
608
|
* coordinator's bound policy inputs change (`updateToolConfig`,
|
|
@@ -602,6 +612,23 @@ export interface ToolkitCoordinatorApi {
|
|
|
602
612
|
* call `decideToolPolicy(...)` with their level / scope.
|
|
603
613
|
*/
|
|
604
614
|
onPolicyChange(listener: ToolPolicyChangeListener): () => void;
|
|
615
|
+
/**
|
|
616
|
+
* Subscribe to accessibility-catalog registrations and removals.
|
|
617
|
+
*
|
|
618
|
+
* The companion to {@link onPolicyChange} for the other mutable input a
|
|
619
|
+
* capability's visibility depends on. A region that renders a catalog card
|
|
620
|
+
* needs both: policy decides whether the learner may have it, catalogs decide
|
|
621
|
+
* whether the content exists, and catalogs arrive on an item shell's mount
|
|
622
|
+
* event — after a card alongside that item has already computed its first
|
|
623
|
+
* answer. Listeners re-query through
|
|
624
|
+
* {@link accessibilityCatalogResolver}; the event names only what changed.
|
|
625
|
+
*
|
|
626
|
+
* Required, unlike {@link decideFeaturePolicy}: this ships with its only
|
|
627
|
+
* consumer, so there are no pre-existing stubs to stay assignable to, and
|
|
628
|
+
* `AGENTS.md` rules out adding an internal-API compatibility shim without a
|
|
629
|
+
* documented exception.
|
|
630
|
+
*/
|
|
631
|
+
onCatalogsChange(listener: CatalogChangeListener): () => void;
|
|
605
632
|
/**
|
|
606
633
|
* Bind (or clear) the active assessment for PNP/profile policy decisions.
|
|
607
634
|
*
|
|
@@ -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: ("braille" | "textToSpeech" | "graph" | "periodicTable" | "calculator" | "answerEliminator" | "ruler" | "protractor" | "graphingCalculator" | "readAloud" | "strikethrough" | "answerMasking" | "readingMask" | "readingGuide" | "readingRuler" | "highContrastDisplay" | "colorContrast" | "invertColors" | "highlighting" | "annotations" | "magnification" | "screenMagnifier" | "zoomable" | "highContrastAudio" | "displayTransformability" | "largePrint" | "fontEnlargement" | "resizeText" | "alternativeText" | "longDescription" | "describedMath" | "tactileGraphic" | "tactileObject" | "audioDescription" | "humanVoice" | "syntheticVoice" | "speechRate" | "speechVolume" | "voicePitch" | "captions" | "closedCaptions" | "openCaptions" | "
|
|
170
|
+
export declare const ALL_STANDARD_ACCESS_FEATURES: ("transcript" | "braille" | "textToSpeech" | "graph" | "periodicTable" | "calculator" | "answerEliminator" | "ruler" | "protractor" | "graphingCalculator" | "readAloud" | "strikethrough" | "answerMasking" | "readingMask" | "readingGuide" | "readingRuler" | "highContrastDisplay" | "colorContrast" | "invertColors" | "highlighting" | "annotations" | "signLanguage" | "magnification" | "screenMagnifier" | "zoomable" | "highContrastAudio" | "displayTransformability" | "largePrint" | "fontEnlargement" | "resizeText" | "alternativeText" | "longDescription" | "describedMath" | "tactileGraphic" | "tactileObject" | "audioDescription" | "humanVoice" | "syntheticVoice" | "speechRate" | "speechVolume" | "voicePitch" | "captions" | "closedCaptions" | "openCaptions" | "subtitles" | "audioControl" | "noBackgroundAudio" | "keyboardControl" | "mouseControl" | "touchControl" | "voiceControl" | "switchControl" | "eyeGazeControl" | "singleSwitchAccess" | "stickyKeys" | "keyboardShortcuts" | "timingControl" | "unlimitedTime" | "extendedTime" | "pauseControl" | "simplifiedLanguage" | "reducedComplexity" | "structuralNavigation" | "tableOfContents" | "reducedDistraction" | "noFlashing" | "pauseAnimation" | "bookmarking" | "guidedNavigation" | "thesaurus" | "spellingAssistance" | "grammarAssistance" | "lineSpacing" | "wordSpacing" | "letterSpacing" | "fontFamily" | "wordHighlighting" | "lineHighlighting" | "focusIndicator" | "printableResource" | "nemeth" | "refreshableBraille" | "index" | "pageNavigation" | "skipContent" | "breadcrumbs" | "searchable" | "fullTextSearch" | "multilingualText" | "translatedText" | "glossary" | "signLanguageInterpretation" | "visualLanguage" | "formulaSheet" | "itemGlossary" | "tutorialAvailable")[];
|
|
171
171
|
/**
|
|
172
172
|
* Example PNP configurations for common accessibility needs
|
|
173
173
|
* These are NOT official profiles but illustrative examples showing
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-language catalog card payloads.
|
|
3
|
+
*
|
|
4
|
+
* A `sign-language` catalog card carries a signing video rather than text, so
|
|
5
|
+
* `CatalogCard.content` (a flat string) cannot describe it: no second source,
|
|
6
|
+
* no MIME type, no poster, no time range. `CatalogCard.payload` carries those,
|
|
7
|
+
* and this module is the single place that decides whether a card describes a
|
|
8
|
+
* playable signed alternate.
|
|
9
|
+
*
|
|
10
|
+
* The payload is the *only* accepted form. A bare URL in `content` is not a
|
|
11
|
+
* signing card: nothing writes that shape, so accepting it would buy a second
|
|
12
|
+
* code path and a second source of truth for no existing content. Such a card
|
|
13
|
+
* is reported and treated as absent rather than silently rendering.
|
|
14
|
+
*
|
|
15
|
+
* Validation is "treat as absent, never as text": a malformed payload must not
|
|
16
|
+
* degrade to an empty video or render a URL as visible content.
|
|
17
|
+
*
|
|
18
|
+
* There is deliberately no signing equivalent of `data-tts-suppress`. Whether a
|
|
19
|
+
* clip gives a decoding item away depends on fingerspelling versus lexical
|
|
20
|
+
* signing — a fact about the recording, known to the signer and not to whoever
|
|
21
|
+
* authors an attribute. And suppression is per node while a signed alternate is
|
|
22
|
+
* one video per item, so the only available rule would withhold a deaf
|
|
23
|
+
* candidate's whole translation over one word. Read-aloud needs a machine-readable
|
|
24
|
+
* guard because a synthesizer speaks whatever text is present; signed content does
|
|
25
|
+
* not exist until a signer films it. Revisit only if per-node signing docking
|
|
26
|
+
* lands and a program authors signing for decoding-construct items.
|
|
27
|
+
*
|
|
28
|
+
* Part of PIE Assessment Toolkit.
|
|
29
|
+
*/
|
|
30
|
+
import type { CatalogCard, CatalogCardPayload, MediaFragmentRange, MediaSource } from "@pie-players/pie-players-shared/types";
|
|
31
|
+
import { applyMediaFragment } from "./catalog-media.js";
|
|
32
|
+
export { applyMediaFragment };
|
|
33
|
+
/** Catalog type token for signed alternates. Matches QTI 3's `support` value. */
|
|
34
|
+
export declare const SIGN_LANGUAGE_CATALOG_TYPE = "sign-language";
|
|
35
|
+
/** ISO 639-3 code for American Sign Language, and QTI 3's `xml:lang` value. */
|
|
36
|
+
export declare const AMERICAN_SIGN_LANGUAGE = "ase";
|
|
37
|
+
export declare function describeSignLanguage(signLang?: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* A validated signed alternate, flattened for rendering.
|
|
40
|
+
*
|
|
41
|
+
* `signLang` is optional because a card need not assert a language. Callers
|
|
42
|
+
* decide what to do with an unknown one; see `matchesRequestedSignLanguage`.
|
|
43
|
+
*/
|
|
44
|
+
export interface SignLanguageMedia {
|
|
45
|
+
signLang?: string;
|
|
46
|
+
sources: MediaSource[];
|
|
47
|
+
poster?: string;
|
|
48
|
+
label?: string;
|
|
49
|
+
fragment?: MediaFragmentRange;
|
|
50
|
+
}
|
|
51
|
+
type SignLanguageCardLike = {
|
|
52
|
+
language?: string;
|
|
53
|
+
content?: string;
|
|
54
|
+
payload?: CatalogCardPayload;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Validate a `sign-language` card into something renderable, or `null`.
|
|
58
|
+
*
|
|
59
|
+
* Returns `null` — meaning "this card is absent" — when the payload yields no
|
|
60
|
+
* usable source. Never returns a partially-valid result that would render an
|
|
61
|
+
* empty player.
|
|
62
|
+
*
|
|
63
|
+
* A payload that carries sources but no `signLang` falls back to the card's
|
|
64
|
+
* `language`, and then to unlabelled. Language *matching* is a separate
|
|
65
|
+
* decision (see `matchesRequestedSignLanguage`) so the strict
|
|
66
|
+
* no-cross-sign-language rule lives in exactly one place.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveSignLanguageMedia(card: SignLanguageCardLike | null | undefined): SignLanguageMedia | null;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a resolved signed alternate may be shown for a requested sign
|
|
71
|
+
* language.
|
|
72
|
+
*
|
|
73
|
+
* Deliberately strict: there is no cross-sign-language fallback. ASL, BSL and
|
|
74
|
+
* LSF are not interchangeable, so showing a different sign language than the
|
|
75
|
+
* one requested would hand a learner a language they may not follow — worse
|
|
76
|
+
* than showing nothing. A card that asserts *no* language is accepted, because it
|
|
77
|
+
* cannot be shown to be a mismatch — only a positive claim of another language
|
|
78
|
+
* is refused.
|
|
79
|
+
*/
|
|
80
|
+
export declare function matchesRequestedSignLanguage(media: SignLanguageMedia, requestedSignLang: string): boolean;
|
|
81
|
+
/** Whether a catalog card is a sign-language card at all. */
|
|
82
|
+
export declare function isSignLanguageCard(card: CatalogCard): boolean;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-language catalog card payloads.
|
|
3
|
+
*
|
|
4
|
+
* A `sign-language` catalog card carries a signing video rather than text, so
|
|
5
|
+
* `CatalogCard.content` (a flat string) cannot describe it: no second source,
|
|
6
|
+
* no MIME type, no poster, no time range. `CatalogCard.payload` carries those,
|
|
7
|
+
* and this module is the single place that decides whether a card describes a
|
|
8
|
+
* playable signed alternate.
|
|
9
|
+
*
|
|
10
|
+
* The payload is the *only* accepted form. A bare URL in `content` is not a
|
|
11
|
+
* signing card: nothing writes that shape, so accepting it would buy a second
|
|
12
|
+
* code path and a second source of truth for no existing content. Such a card
|
|
13
|
+
* is reported and treated as absent rather than silently rendering.
|
|
14
|
+
*
|
|
15
|
+
* Validation is "treat as absent, never as text": a malformed payload must not
|
|
16
|
+
* degrade to an empty video or render a URL as visible content.
|
|
17
|
+
*
|
|
18
|
+
* There is deliberately no signing equivalent of `data-tts-suppress`. Whether a
|
|
19
|
+
* clip gives a decoding item away depends on fingerspelling versus lexical
|
|
20
|
+
* signing — a fact about the recording, known to the signer and not to whoever
|
|
21
|
+
* authors an attribute. And suppression is per node while a signed alternate is
|
|
22
|
+
* one video per item, so the only available rule would withhold a deaf
|
|
23
|
+
* candidate's whole translation over one word. Read-aloud needs a machine-readable
|
|
24
|
+
* guard because a synthesizer speaks whatever text is present; signed content does
|
|
25
|
+
* not exist until a signer films it. Revisit only if per-node signing docking
|
|
26
|
+
* lands and a program authors signing for decoding-construct items.
|
|
27
|
+
*
|
|
28
|
+
* Part of PIE Assessment Toolkit.
|
|
29
|
+
*/
|
|
30
|
+
import { applyMediaFragment, isSafeMediaSrc, normalizeMediaFragment, normalizeMediaSources, trimmedOrUndefined, } from "./catalog-media.js";
|
|
31
|
+
// Re-exported because `applyMediaFragment` is part of the toolkit's public
|
|
32
|
+
// surface under this module's name; the implementation is shared with spoken
|
|
33
|
+
// audio cards now that both forms reference media.
|
|
34
|
+
export { applyMediaFragment };
|
|
35
|
+
/** Catalog type token for signed alternates. Matches QTI 3's `support` value. */
|
|
36
|
+
export const SIGN_LANGUAGE_CATALOG_TYPE = "sign-language";
|
|
37
|
+
/** ISO 639-3 code for American Sign Language, and QTI 3's `xml:lang` value. */
|
|
38
|
+
export const AMERICAN_SIGN_LANGUAGE = "ase";
|
|
39
|
+
/**
|
|
40
|
+
* Human-readable names for the sign languages PIE has a reason to name today.
|
|
41
|
+
*
|
|
42
|
+
* Used for the region's accessible label, which must name the language ("American
|
|
43
|
+
* Sign Language") rather than say "video". Unknown codes fall back to a labelled
|
|
44
|
+
* code rather than a lie.
|
|
45
|
+
*/
|
|
46
|
+
const SIGN_LANGUAGE_NAMES = {
|
|
47
|
+
ase: "American Sign Language",
|
|
48
|
+
bfi: "British Sign Language",
|
|
49
|
+
fsl: "French Sign Language",
|
|
50
|
+
gss: "Greek Sign Language",
|
|
51
|
+
mfs: "Mexican Sign Language",
|
|
52
|
+
// Signed English is not a distinct sign language; it rides the same card
|
|
53
|
+
// type tagged with a spoken-language code, so name it where it appears.
|
|
54
|
+
"eng-US": "Signed English",
|
|
55
|
+
"en-US": "Signed English",
|
|
56
|
+
};
|
|
57
|
+
export function describeSignLanguage(signLang) {
|
|
58
|
+
const code = (signLang || "").trim();
|
|
59
|
+
if (!code)
|
|
60
|
+
return "Sign language";
|
|
61
|
+
return SIGN_LANGUAGE_NAMES[code] ?? `Sign language (${code})`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate a `sign-language` card into something renderable, or `null`.
|
|
65
|
+
*
|
|
66
|
+
* Returns `null` — meaning "this card is absent" — when the payload yields no
|
|
67
|
+
* usable source. Never returns a partially-valid result that would render an
|
|
68
|
+
* empty player.
|
|
69
|
+
*
|
|
70
|
+
* A payload that carries sources but no `signLang` falls back to the card's
|
|
71
|
+
* `language`, and then to unlabelled. Language *matching* is a separate
|
|
72
|
+
* decision (see `matchesRequestedSignLanguage`) so the strict
|
|
73
|
+
* no-cross-sign-language rule lives in exactly one place.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveSignLanguageMedia(card) {
|
|
76
|
+
if (!card)
|
|
77
|
+
return null;
|
|
78
|
+
const cardLanguage = trimmedOrUndefined(card.language);
|
|
79
|
+
const payload = card.payload;
|
|
80
|
+
if (!payload || typeof payload !== "object") {
|
|
81
|
+
// Always say something. A signing card that yields nothing is invisible to
|
|
82
|
+
// everyone except the learner who needed the accommodation, so the only
|
|
83
|
+
// place it can surface is here.
|
|
84
|
+
//
|
|
85
|
+
// `payload` was briefly also spelled `signLanguage` by two producers, and
|
|
86
|
+
// this function accepted both. It no longer does — one fact under two names
|
|
87
|
+
// is what let a card render on one code path and read as absent on another —
|
|
88
|
+
// so a card left over from that spelling arrives here with no payload at
|
|
89
|
+
// all, which is what the second message is for.
|
|
90
|
+
if (trimmedOrUndefined(card.content)) {
|
|
91
|
+
console.warn("[sign-language] card carries `content` but no `payload`; signing media must be a structured payload, so this card is ignored");
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.warn("[sign-language] card carries no `payload`; signing media lives in `payload` (a card written against the older `signLanguage` key needs re-importing), so this card is ignored");
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const media = payload.media;
|
|
99
|
+
const sources = normalizeMediaSources(media?.sources);
|
|
100
|
+
if (sources.length === 0)
|
|
101
|
+
return null;
|
|
102
|
+
const poster = trimmedOrUndefined(media?.poster);
|
|
103
|
+
return {
|
|
104
|
+
signLang: trimmedOrUndefined(payload.signLang) ?? cardLanguage,
|
|
105
|
+
sources,
|
|
106
|
+
poster: poster && isSafeMediaSrc(poster) ? poster : undefined,
|
|
107
|
+
label: trimmedOrUndefined(media?.label),
|
|
108
|
+
fragment: normalizeMediaFragment(payload.fragment),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Whether a resolved signed alternate may be shown for a requested sign
|
|
113
|
+
* language.
|
|
114
|
+
*
|
|
115
|
+
* Deliberately strict: there is no cross-sign-language fallback. ASL, BSL and
|
|
116
|
+
* LSF are not interchangeable, so showing a different sign language than the
|
|
117
|
+
* one requested would hand a learner a language they may not follow — worse
|
|
118
|
+
* than showing nothing. A card that asserts *no* language is accepted, because it
|
|
119
|
+
* cannot be shown to be a mismatch — only a positive claim of another language
|
|
120
|
+
* is refused.
|
|
121
|
+
*/
|
|
122
|
+
export function matchesRequestedSignLanguage(media, requestedSignLang) {
|
|
123
|
+
if (!media.signLang)
|
|
124
|
+
return true;
|
|
125
|
+
const requested = (requestedSignLang || "").trim();
|
|
126
|
+
if (!requested)
|
|
127
|
+
return true;
|
|
128
|
+
return media.signLang.toLowerCase() === requested.toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
/** Whether a catalog card is a sign-language card at all. */
|
|
131
|
+
export function isSignLanguageCard(card) {
|
|
132
|
+
return card?.catalog === SIGN_LANGUAGE_CATALOG_TYPE;
|
|
133
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recorded audio as a `spoken` catalog card.
|
|
3
|
+
*
|
|
4
|
+
* QTI 3 treats a recording and synthesized speech as the *same* support — both
|
|
5
|
+
* are `spoken`, and a card carries recorded audio through `qti-file-href` plus a
|
|
6
|
+
* MIME type — so this is not a new accommodation but the other form the existing
|
|
7
|
+
* one can take. Some programs prefer a human voice to synthesis; PIE's `spoken`
|
|
8
|
+
* card was string-only, so it had no way to say "play this file for this node".
|
|
9
|
+
*
|
|
10
|
+
* A node commonly carries both forms in the same language: the reading script
|
|
11
|
+
* *and* a recording of it. That is APIP's pattern and what QTI's migration
|
|
12
|
+
* guidance preserves, because the script is both the source the audio was
|
|
13
|
+
* generated from and the fallback for when the audio will not play. Resolution
|
|
14
|
+
* chooses between them with `CatalogLookupOptions.form`; playback treats the
|
|
15
|
+
* script as the recording's fallback.
|
|
16
|
+
*
|
|
17
|
+
* Validation posture matches sign-language cards: "treat as absent, never as
|
|
18
|
+
* partially valid". A malformed payload must not produce a silent player that
|
|
19
|
+
* looks like read-aloud is working.
|
|
20
|
+
*
|
|
21
|
+
* Part of PIE Assessment Toolkit.
|
|
22
|
+
*/
|
|
23
|
+
import type { CatalogCardPayload, MediaFragmentRange, MediaSource } from "@pie-players/pie-players-shared/types";
|
|
24
|
+
/** Catalog type token for spoken alternates. Matches QTI 3's `support` value. */
|
|
25
|
+
export declare const SPOKEN_CATALOG_TYPE = "spoken";
|
|
26
|
+
/** A validated recorded spoken alternate, flattened for playback. */
|
|
27
|
+
export interface SpokenAudioMedia {
|
|
28
|
+
/**
|
|
29
|
+
* Authored order preserved. Playback uses the first entry: an `<audio>` element
|
|
30
|
+
* fed alternative `<source>` children reports failure through a path that is
|
|
31
|
+
* awkward to observe reliably, and a dependable fallback to the reading script
|
|
32
|
+
* is worth more than encoding negotiation. Extra entries are kept so a future
|
|
33
|
+
* consumer can negotiate without re-reading the card.
|
|
34
|
+
*/
|
|
35
|
+
sources: MediaSource[];
|
|
36
|
+
fragment?: MediaFragmentRange;
|
|
37
|
+
label?: string;
|
|
38
|
+
}
|
|
39
|
+
type SpokenAudioCardLike = {
|
|
40
|
+
language?: string;
|
|
41
|
+
content?: string;
|
|
42
|
+
payload?: CatalogCardPayload;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Validate a `spoken` card's payload into something playable, or `null`.
|
|
46
|
+
*
|
|
47
|
+
* Silent when the card simply is not a recording — a card carrying `content` is
|
|
48
|
+
* a reading script, which is the overwhelmingly common case and not a fault.
|
|
49
|
+
* Loud when a card looks like it meant to be a recording and cannot be played,
|
|
50
|
+
* because that failure is otherwise invisible to everyone but the learner who
|
|
51
|
+
* needed it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveSpokenAudioMedia(card: SpokenAudioCardLike | null | undefined): SpokenAudioMedia | null;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recorded audio as a `spoken` catalog card.
|
|
3
|
+
*
|
|
4
|
+
* QTI 3 treats a recording and synthesized speech as the *same* support — both
|
|
5
|
+
* are `spoken`, and a card carries recorded audio through `qti-file-href` plus a
|
|
6
|
+
* MIME type — so this is not a new accommodation but the other form the existing
|
|
7
|
+
* one can take. Some programs prefer a human voice to synthesis; PIE's `spoken`
|
|
8
|
+
* card was string-only, so it had no way to say "play this file for this node".
|
|
9
|
+
*
|
|
10
|
+
* A node commonly carries both forms in the same language: the reading script
|
|
11
|
+
* *and* a recording of it. That is APIP's pattern and what QTI's migration
|
|
12
|
+
* guidance preserves, because the script is both the source the audio was
|
|
13
|
+
* generated from and the fallback for when the audio will not play. Resolution
|
|
14
|
+
* chooses between them with `CatalogLookupOptions.form`; playback treats the
|
|
15
|
+
* script as the recording's fallback.
|
|
16
|
+
*
|
|
17
|
+
* Validation posture matches sign-language cards: "treat as absent, never as
|
|
18
|
+
* partially valid". A malformed payload must not produce a silent player that
|
|
19
|
+
* looks like read-aloud is working.
|
|
20
|
+
*
|
|
21
|
+
* Part of PIE Assessment Toolkit.
|
|
22
|
+
*/
|
|
23
|
+
import { normalizeMediaFragment, normalizeMediaSources, trimmedOrUndefined, } from "./catalog-media.js";
|
|
24
|
+
/** Catalog type token for spoken alternates. Matches QTI 3's `support` value. */
|
|
25
|
+
export const SPOKEN_CATALOG_TYPE = "spoken";
|
|
26
|
+
/**
|
|
27
|
+
* Validate a `spoken` card's payload into something playable, or `null`.
|
|
28
|
+
*
|
|
29
|
+
* Silent when the card simply is not a recording — a card carrying `content` is
|
|
30
|
+
* a reading script, which is the overwhelmingly common case and not a fault.
|
|
31
|
+
* Loud when a card looks like it meant to be a recording and cannot be played,
|
|
32
|
+
* because that failure is otherwise invisible to everyone but the learner who
|
|
33
|
+
* needed it.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveSpokenAudioMedia(card) {
|
|
36
|
+
if (!card)
|
|
37
|
+
return null;
|
|
38
|
+
const payload = card.payload;
|
|
39
|
+
if (!payload || typeof payload !== "object") {
|
|
40
|
+
// A script card, not a broken audio card. Resolution asks for the payload
|
|
41
|
+
// form as a *preference*, so getting a `content` card back here is normal.
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const media = payload.media;
|
|
45
|
+
if (!media || typeof media !== "object") {
|
|
46
|
+
console.warn("[spoken-audio] card carries a `payload` with no `media`; recorded speech needs `media.sources`, so this card is ignored and read-aloud falls back to the script or generated speech");
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// A signing video filed under `spoken` is a mis-authored card, not an audio
|
|
50
|
+
// track to guess at. Refusing it keeps the two card types from quietly
|
|
51
|
+
// swapping roles.
|
|
52
|
+
if (media.kind !== undefined && media.kind !== "audio") {
|
|
53
|
+
console.warn(`[spoken-audio] card's media is kind "${media.kind}", not "audio"; a spoken card carries recorded speech, so this card is ignored`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const sources = normalizeMediaSources(media.sources);
|
|
57
|
+
if (sources.length === 0) {
|
|
58
|
+
console.warn("[spoken-audio] card's `media.sources` yielded no usable URL; read-aloud falls back to the script or generated speech");
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
sources,
|
|
63
|
+
fragment: normalizeMediaFragment(payload.fragment),
|
|
64
|
+
label: trimmedOrUndefined(media.label),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canonicalizeMathML } from "./mathml-sanitization.js";
|
|
2
|
-
import { collectVisibleTextAndMap,
|
|
2
|
+
import { collectVisibleTextAndMap, isNodeExcludedFromSpeech, normalizeTextForSpeech, shouldInsertWordBoundarySpace, } from "./text-processing.js";
|
|
3
3
|
const createAccumulator = (options) => ({
|
|
4
4
|
chars: [],
|
|
5
5
|
map: new Map(),
|
|
@@ -226,7 +226,7 @@ const textFallbackFromMathML = (mathml) => {
|
|
|
226
226
|
const collectVisibleMathFallback = (element, canonicalMathML, options) => {
|
|
227
227
|
const mathAcc = createAccumulator(options);
|
|
228
228
|
const visit = (node) => {
|
|
229
|
-
if (
|
|
229
|
+
if (isNodeExcludedFromSpeech(node, element))
|
|
230
230
|
return;
|
|
231
231
|
if (node.nodeType === 3) {
|
|
232
232
|
appendTextNode(mathAcc, node);
|
|
@@ -284,7 +284,7 @@ const collectMathAware = (root, options) => {
|
|
|
284
284
|
textChunkSourceElement = undefined;
|
|
285
285
|
};
|
|
286
286
|
const processNode = (node) => {
|
|
287
|
-
if (
|
|
287
|
+
if (isNodeExcludedFromSpeech(node, root))
|
|
288
288
|
return;
|
|
289
289
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
290
290
|
const sourceElement = resolveTextChunkSourceElement(node, root);
|
|
@@ -11,6 +11,57 @@ export declare const normalizeTextForSpeech: (text: string) => string;
|
|
|
11
11
|
export declare const createSpeechAlignmentTokenPattern: () => RegExp;
|
|
12
12
|
export declare const isElementHiddenForTTS: (element: Element) => boolean;
|
|
13
13
|
export declare const isNodeHiddenForTTS: (node: Node, root?: Element | null) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Marks content that must be shown but never spoken — items where reading *is*
|
|
16
|
+
* the construct, such as decoding and spelling, where speaking the node hands
|
|
17
|
+
* over the answer.
|
|
18
|
+
*
|
|
19
|
+
* Not a PNP field: `prohibitedSupports` is the learner declining a support, while
|
|
20
|
+
* this is the item saying "not here, for anyone", so it overrides an entitlement
|
|
21
|
+
* rather than yielding to it.
|
|
22
|
+
*
|
|
23
|
+
* Shape follows QTI 3's `data-qti-suppress-tts` — an attribute on the content
|
|
24
|
+
* element, single-valued, vocabulary below. Element placement is what makes it
|
|
25
|
+
* work on undocked nodes and enforceable in the selection read-aloud path, which
|
|
26
|
+
* consults no catalog. The name follows PIE's `data-tts-*` family, and PIE reads
|
|
27
|
+
* only this spelling; importers map QTI's.
|
|
28
|
+
*/
|
|
29
|
+
export declare const TTS_SUPPRESS_ATTRIBUTE = "data-tts-suppress";
|
|
30
|
+
/**
|
|
31
|
+
* Whether this element forbids machine read-aloud of itself and its subtree.
|
|
32
|
+
*
|
|
33
|
+
* Unrecognized and empty values suppress rather than pass through, and say so once
|
|
34
|
+
* per distinct value: a typo that fell through would speak a word the item was
|
|
35
|
+
* measuring, invalidating the score with no visible symptom, whereas
|
|
36
|
+
* over-suppressing only withholds speech an author had already marked as withheld.
|
|
37
|
+
*/
|
|
38
|
+
export declare const isElementSuppressedForTTS: (element: Element) => boolean;
|
|
39
|
+
export declare const isNodeSuppressedForTTS: (node: Node, root?: Element | null) => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The predicate every speech-producing path filters on: hidden *or* suppressed.
|
|
42
|
+
*
|
|
43
|
+
* Kept distinct from `isNodeHiddenForTTS`, which stays a question about
|
|
44
|
+
* visibility — suppressed content is visible on purpose, and the highlight
|
|
45
|
+
* geometry resolvers that ask "can the candidate see this" must keep getting
|
|
46
|
+
* the visibility answer rather than this one.
|
|
47
|
+
*/
|
|
48
|
+
export declare const isNodeExcludedFromSpeech: (node: Node, root?: Element | null) => boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Text of a range with the parts that must not be spoken removed.
|
|
51
|
+
*
|
|
52
|
+
* `Range.toString()` is not usable for speech: it is pure character extraction
|
|
53
|
+
* and honours no DOM filter at all, so it happily returns suppressed — and
|
|
54
|
+
* hidden — text. The selection read-aloud path is a text-in path rather than a
|
|
55
|
+
* DOM walk, which makes this the only place its content can be filtered.
|
|
56
|
+
*
|
|
57
|
+
* `filtered` reports whether anything was dropped, so a caller can tell "the
|
|
58
|
+
* candidate selected nothing speakable" apart from "the candidate selected
|
|
59
|
+
* nothing".
|
|
60
|
+
*/
|
|
61
|
+
export declare const collectRangeTextForSpeech: (range: Range, root: Element) => {
|
|
62
|
+
text: string;
|
|
63
|
+
filtered: boolean;
|
|
64
|
+
};
|
|
14
65
|
export declare const shouldInsertWordBoundarySpace: (previousChar: string | null, nextChar: string | null, options?: TextProcessingOptions) => boolean;
|
|
15
66
|
export declare const collectVisibleTextAndMap: (element: Element, options?: TextProcessingOptions) => {
|
|
16
67
|
text: string;
|