@intellectif/lk-core 0.15.0 → 0.17.0
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/CHANGELOG.md +74 -0
- package/dist/{activity-C7cR8U9n.d.cts → activity-BmjBjXVJ.d.cts} +218 -3
- package/dist/{activity-C7cR8U9n.d.ts → activity-BmjBjXVJ.d.ts} +218 -3
- package/dist/{chunk-W7AQP6AV.cjs → chunk-2CV6NU32.cjs} +24 -22
- package/dist/chunk-2CV6NU32.cjs.map +1 -0
- package/dist/{chunk-DPHX4E7X.js → chunk-D3GP5MET.js} +718 -662
- package/dist/chunk-D3GP5MET.js.map +1 -0
- package/dist/{chunk-QFWEF5ST.cjs → chunk-D4TOPSHV.cjs} +539 -483
- package/dist/chunk-D4TOPSHV.cjs.map +1 -0
- package/dist/{chunk-7ETU6EXQ.cjs → chunk-EOFWSGLU.cjs} +4 -4
- package/dist/{chunk-7ETU6EXQ.cjs.map → chunk-EOFWSGLU.cjs.map} +1 -1
- package/dist/{chunk-L6YBQXSB.js → chunk-WXS5EB65.js} +2 -2
- package/dist/{chunk-54XYQ3CS.js → chunk-X5DAXBJI.js} +2 -2
- package/dist/{chunk-5KWF34LJ.cjs → chunk-XNMKDFPW.cjs} +31 -31
- package/dist/{chunk-5KWF34LJ.cjs.map → chunk-XNMKDFPW.cjs.map} +1 -1
- package/dist/{chunk-EEMVWBPP.js → chunk-Z7QQZ53L.js} +10 -8
- package/dist/chunk-Z7QQZ53L.js.map +1 -0
- package/dist/{index-BWyMDlUS.d.cts → index-CvfeD_cU.d.cts} +144 -86
- package/dist/{index-D10NaztZ.d.ts → index-G_KusUcK.d.ts} +144 -86
- package/dist/index.cjs +544 -128
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +135 -5
- package/dist/index.d.ts +135 -5
- package/dist/index.js +426 -10
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +5 -3
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +4 -2
- package/dist/scoring.cjs +3 -3
- package/dist/scoring.d.cts +2 -2
- package/dist/scoring.d.ts +2 -2
- package/dist/scoring.js +2 -2
- package/dist/xapi.cjs +3 -3
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-DPHX4E7X.js.map +0 -1
- package/dist/chunk-EEMVWBPP.js.map +0 -1
- package/dist/chunk-QFWEF5ST.cjs.map +0 -1
- package/dist/chunk-W7AQP6AV.cjs.map +0 -1
- /package/dist/{chunk-L6YBQXSB.js.map → chunk-WXS5EB65.js.map} +0 -0
- /package/dist/{chunk-54XYQ3CS.js.map → chunk-X5DAXBJI.js.map} +0 -0
|
@@ -1,42 +1,205 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/schemas/ai.ts
|
|
2
|
+
var _v4 = require('zod/v4');
|
|
3
|
+
var AiPermissionsSchema = _v4.z.looseObject({
|
|
4
|
+
explanations: _v4.z.boolean().optional(),
|
|
5
|
+
hints: _v4.z.boolean().optional()
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/schemas/feedback.ts
|
|
9
|
+
|
|
10
|
+
var FeedbackSchema = _v4.z.looseObject({
|
|
11
|
+
correct: _v4.z.string().min(1).optional(),
|
|
12
|
+
incorrect: _v4.z.string().min(1).optional()
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// src/schemas/media.ts
|
|
16
|
+
|
|
17
|
+
var MediaUrlSchema = _v4.z.union([
|
|
18
|
+
_v4.z.url().refine((value) => /^(https?|data|blob):/i.test(value), {
|
|
19
|
+
error: "Absolute media URLs must use the https:, http:, data:, or blob: scheme."
|
|
20
|
+
}),
|
|
21
|
+
_v4.z.string().regex(/^\/(?!\/)\S*$/, {
|
|
22
|
+
error: 'Relative media URLs must be root-relative (a single leading "/").'
|
|
23
|
+
})
|
|
24
|
+
]);
|
|
25
|
+
var NativeControlHintSchema = _v4.z.enum(["hide-download", "hide-rate"]);
|
|
26
|
+
var MediaPlaybackSchema = _v4.z.strictObject({
|
|
27
|
+
/**
|
|
28
|
+
* `native` (the resolved default) renders today's `<audio controls>`
|
|
29
|
+
* unchanged. `minimal` — resolved automatically whenever any enforcement
|
|
30
|
+
* field is set — replaces the browser bar with the SDK transport:
|
|
31
|
+
* play/pause, elapsed/total, mute, volume, optional speed, optional
|
|
32
|
+
* scrubber, and a live plays-remaining status.
|
|
33
|
+
*/
|
|
34
|
+
controls: _v4.z.enum(["native", "minimal"]).optional(),
|
|
35
|
+
/**
|
|
36
|
+
* How many times the recording may be STARTED. A play is consumed when
|
|
37
|
+
* playback begins from anywhere other than where it last stopped, so
|
|
38
|
+
* pausing, resuming, and paging between the questions of one listening
|
|
39
|
+
* group are all free. Enforced in `practice` and `exam`; never in `review`.
|
|
40
|
+
*/
|
|
41
|
+
maxPlays: _v4.z.number().int().min(1).max(20).optional(),
|
|
42
|
+
/** `none` renders no scrubber and reverts an out-of-band seek to the high-water mark. */
|
|
43
|
+
seek: _v4.z.enum(["allow", "none"]).optional(),
|
|
44
|
+
/** `fixed` renders no speed control and snaps `playbackRate` back to 1. */
|
|
45
|
+
rate: _v4.z.enum(["allow", "fixed"]).optional(),
|
|
46
|
+
/** Advisory only. See {@link NativeControlHintSchema}. */
|
|
47
|
+
nativeControlHints: _v4.z.array(NativeControlHintSchema).min(1).max(2).optional()
|
|
48
|
+
});
|
|
49
|
+
var MediaTrackSchema = _v4.z.strictObject({
|
|
50
|
+
kind: _v4.z.enum(["captions", "subtitles"]),
|
|
51
|
+
src: MediaUrlSchema,
|
|
52
|
+
srclang: _v4.z.string().regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, {
|
|
53
|
+
error: 'srclang must be a BCP 47 language tag, such as "en", "es" or "pt-BR".'
|
|
54
|
+
}),
|
|
55
|
+
label: _v4.z.string().max(60).refine((label) => label.trim().length > 0, { error: "A track label must not be empty." }),
|
|
56
|
+
default: _v4.z.boolean().optional()
|
|
57
|
+
});
|
|
58
|
+
var MAX_TRACKS = 12;
|
|
59
|
+
function posterFits(m) {
|
|
60
|
+
return m.poster === void 0 || m.type === "video";
|
|
61
|
+
}
|
|
62
|
+
function tracksFit(m) {
|
|
63
|
+
return m.tracks === void 0 || m.type === "audio" || m.type === "video";
|
|
64
|
+
}
|
|
65
|
+
function tracksAreDistinct(m) {
|
|
66
|
+
if (m.tracks === void 0) {
|
|
67
|
+
return true;
|
|
8
68
|
}
|
|
9
|
-
|
|
10
|
-
|
|
69
|
+
const defaults = m.tracks.filter((track) => track.default === true).length;
|
|
70
|
+
const keys = new Set(m.tracks.map((track) => `${track.kind}:${track.srclang.toLowerCase()}`));
|
|
71
|
+
return defaults <= 1 && keys.size === m.tracks.length;
|
|
72
|
+
}
|
|
73
|
+
var POSTER_FITS = { error: "poster is for video media only.", path: ["poster"] };
|
|
74
|
+
var TRACKS_FIT = { error: "tracks are for audio and video media only.", path: ["tracks"] };
|
|
75
|
+
var TRACKS_DISTINCT = {
|
|
76
|
+
error: "tracks: at most one may be the default, and no two may share a kind and a language \u2014 a menu listing one language twice cannot say which one it plays.",
|
|
77
|
+
path: ["tracks"]
|
|
11
78
|
};
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
79
|
+
var MediaSchema = _v4.z.looseObject({
|
|
80
|
+
type: _v4.z.enum(["image", "audio", "video", "embed"]),
|
|
81
|
+
url: MediaUrlSchema,
|
|
82
|
+
alt: _v4.z.string().min(1).optional(),
|
|
83
|
+
captionsUrl: MediaUrlSchema.optional(),
|
|
84
|
+
tracks: _v4.z.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
|
|
85
|
+
poster: MediaUrlSchema.optional(),
|
|
86
|
+
playback: MediaPlaybackSchema.optional()
|
|
87
|
+
}).refine(
|
|
88
|
+
(m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
|
|
89
|
+
{
|
|
90
|
+
error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
|
|
91
|
+
path: ["alt"]
|
|
17
92
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
93
|
+
).refine((m) => m.type !== "embed" || /^https?:\/\//i.test(m.url), {
|
|
94
|
+
error: "embed media requires an absolute http(s) provider URL. data:, blob:, and relative URLs are not allowed for embeds \u2014 the embed iframe runs with allow-scripts, and a data:/same-origin document there is an XSS vector.",
|
|
95
|
+
path: ["url"]
|
|
96
|
+
}).refine(posterFits, POSTER_FITS).refine(tracksFit, TRACKS_FIT).refine(tracksAreDistinct, TRACKS_DISTINCT).refine((m) => m.playback === void 0 || m.type === "audio", {
|
|
97
|
+
error: "playback policy is supported on audio media only. An embed is a provider iframe the SDK cannot control at all (it has no reliable JS API for a third-party player); an image has nothing to play; video is deliberately deferred, because a video transport must also own fullscreen and Picture-in-Picture and the SDK will not pretend to govern those yet.",
|
|
98
|
+
path: ["playback"]
|
|
99
|
+
}).refine(
|
|
100
|
+
(m) => _optionalChain([m, 'access', _2 => _2.playback, 'optionalAccess', _3 => _3.controls]) !== "native" || m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
|
|
101
|
+
{
|
|
102
|
+
error: 'controls: "native" cannot carry maxPlays, seek: "none" or rate: "fixed". The browser\'s own bar keeps its play button enabled after a budget is spent, and keeps a scrubber that would move and then silently snap back \u2014 a control that looks operable and does nothing (WCAG 3.2.2 / 4.1.3). Omit `controls` and the SDK transport is used automatically, or use nativeControlHints for a cosmetic hint.',
|
|
103
|
+
path: ["playback", "controls"]
|
|
27
104
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.name = "DeferredScoringError";
|
|
105
|
+
).refine((m) => _optionalChain([m, 'access', _4 => _4.playback, 'optionalAccess', _5 => _5.maxPlays]) === void 0 || m.playback.seek !== "allow", {
|
|
106
|
+
error: 'maxPlays cannot be combined with seek: "allow". A play is consumed when playback starts from somewhere other than where it stopped, so scrubbing back mid-play would replay the whole recording without spending anything. Omit `seek` \u2014 it resolves to "none" under a budget.',
|
|
107
|
+
path: ["playback", "seek"]
|
|
108
|
+
}).refine(
|
|
109
|
+
(m) => _optionalChain([m, 'access', _6 => _6.playback, 'optionalAccess', _7 => _7.controls]) !== "minimal" || m.playback.maxPlays !== void 0 || m.playback.seek === "none" || m.playback.rate === "fixed",
|
|
110
|
+
{
|
|
111
|
+
error: 'controls: "minimal" with nothing to enforce trades the browser\'s localized, familiar control bar for the SDK\'s, and buys nothing. Set maxPlays, seek: "none" or rate: "fixed", or omit `controls`.',
|
|
112
|
+
path: ["playback", "controls"]
|
|
37
113
|
}
|
|
38
|
-
|
|
39
|
-
|
|
114
|
+
).refine(
|
|
115
|
+
(m) => _optionalChain([m, 'access', _8 => _8.playback, 'optionalAccess', _9 => _9.nativeControlHints]) === void 0 || m.playback.controls !== "minimal" && m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
|
|
116
|
+
{
|
|
117
|
+
error: "nativeControlHints only affects the browser's own control bar, and an enforcing policy replaces that bar with the SDK transport \u2014 so the hints would be silently inert. Use them on an otherwise unrestricted recording, or drop them.",
|
|
118
|
+
path: ["playback", "nativeControlHints"]
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
var RedactedMediaSchema = _v4.z.strictObject({
|
|
122
|
+
type: _v4.z.enum(["image", "audio", "video", "embed"]),
|
|
123
|
+
url: MediaUrlSchema,
|
|
124
|
+
alt: _v4.z.string().min(1).optional(),
|
|
125
|
+
captionsUrl: MediaUrlSchema.optional(),
|
|
126
|
+
tracks: _v4.z.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
|
|
127
|
+
poster: MediaUrlSchema.optional(),
|
|
128
|
+
playback: MediaPlaybackSchema.optional()
|
|
129
|
+
}).refine(
|
|
130
|
+
(m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
|
|
131
|
+
{
|
|
132
|
+
error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
|
|
133
|
+
path: ["alt"]
|
|
134
|
+
}
|
|
135
|
+
).refine((m) => m.type !== "embed" || /^https?:\/\//i.test(m.url), {
|
|
136
|
+
error: "embed media requires an absolute http(s) provider URL. data:, blob:, and relative URLs are not allowed for embeds \u2014 the embed iframe runs with allow-scripts, and a data:/same-origin document there is an XSS vector.",
|
|
137
|
+
path: ["url"]
|
|
138
|
+
}).refine(posterFits, POSTER_FITS).refine(tracksFit, TRACKS_FIT).refine(tracksAreDistinct, TRACKS_DISTINCT);
|
|
139
|
+
|
|
140
|
+
// src/schemas/fill-in-the-blanks.ts
|
|
141
|
+
|
|
142
|
+
var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
143
|
+
var TextMatchPolicySchema = _v4.z.looseObject({
|
|
144
|
+
caseSensitive: _v4.z.boolean().optional(),
|
|
145
|
+
trim: _v4.z.boolean().optional(),
|
|
146
|
+
normalize: _v4.z.enum(["none", "NFC", "NFKC"]).optional(),
|
|
147
|
+
foldDiacritics: _v4.z.boolean().optional(),
|
|
148
|
+
collapseInnerWhitespace: _v4.z.boolean().optional(),
|
|
149
|
+
ignorePunctuation: _v4.z.boolean().optional(),
|
|
150
|
+
levenshtein: _v4.z.number().int().min(0).optional(),
|
|
151
|
+
locale: _v4.z.string().optional()
|
|
152
|
+
});
|
|
153
|
+
var BlankConfigSchema = _v4.z.looseObject({
|
|
154
|
+
id: _v4.z.string().min(1),
|
|
155
|
+
acceptedAnswers: _v4.z.array(
|
|
156
|
+
_v4.z.string().min(1).refine((answer) => answer.trim().length > 0, {
|
|
157
|
+
error: "Accepted answers must contain non-whitespace characters."
|
|
158
|
+
})
|
|
159
|
+
).min(1),
|
|
160
|
+
caseSensitive: _v4.z.boolean().optional(),
|
|
161
|
+
trimWhitespace: _v4.z.boolean().optional(),
|
|
162
|
+
match: TextMatchPolicySchema.optional(),
|
|
163
|
+
hint: _v4.z.string().optional(),
|
|
164
|
+
feedback: _v4.z.string().optional()
|
|
165
|
+
});
|
|
166
|
+
var FillInTheBlanksDataSchema = _v4.z.looseObject({
|
|
167
|
+
schemaVersion: _v4.z.literal("1.0"),
|
|
168
|
+
type: _v4.z.literal("fill-in-the-blanks"),
|
|
169
|
+
id: _v4.z.string().min(1),
|
|
170
|
+
title: _v4.z.string().min(1),
|
|
171
|
+
passage: _v4.z.string().min(1),
|
|
172
|
+
passageHtml: _v4.z.string().optional(),
|
|
173
|
+
blanks: _v4.z.array(BlankConfigSchema).min(1),
|
|
174
|
+
scoringStrategy: _v4.z.enum(["all-or-nothing", "partial"]),
|
|
175
|
+
media: MediaSchema.optional(),
|
|
176
|
+
feedback: FeedbackSchema.optional(),
|
|
177
|
+
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
178
|
+
locale: _v4.z.string().optional(),
|
|
179
|
+
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
180
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
|
|
181
|
+
ai: AiPermissionsSchema.optional()
|
|
182
|
+
}).refine(
|
|
183
|
+
(data) => {
|
|
184
|
+
const placeholderCounts2 = /* @__PURE__ */ new Map();
|
|
185
|
+
for (const match of data.passage.matchAll(PLACEHOLDER_RE)) {
|
|
186
|
+
const id = match[1];
|
|
187
|
+
placeholderCounts2.set(id, (_nullishCoalesce(placeholderCounts2.get(id), () => ( 0))) + 1);
|
|
188
|
+
}
|
|
189
|
+
const blankIds = data.blanks.map((blank) => blank.id);
|
|
190
|
+
if (new Set(blankIds).size !== blankIds.length) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (placeholderCounts2.size !== blankIds.length) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return blankIds.every((id) => placeholderCounts2.get(id) === 1);
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
|
|
200
|
+
path: ["passage"]
|
|
201
|
+
}
|
|
202
|
+
);
|
|
40
203
|
|
|
41
204
|
// src/validation-scope.ts
|
|
42
205
|
var active = null;
|
|
@@ -523,9 +686,9 @@ function keepsInContext(points, index) {
|
|
|
523
686
|
const after = letterAfter(points, index);
|
|
524
687
|
return before !== void 0 && after !== void 0 && inRanges(before, JOINS_FORWARD) && inRanges(after, JOINS_BACKWARD);
|
|
525
688
|
}
|
|
526
|
-
const previousPoint = _optionalChain([previous, 'optionalAccess',
|
|
689
|
+
const previousPoint = _optionalChain([previous, 'optionalAccess', _10 => _10.codePointAt, 'call', _11 => _11(0)]);
|
|
527
690
|
if (point === MONGOLIAN_VOWEL_SEPARATOR) {
|
|
528
|
-
return (isMongolianLetterOrMark(previousPoint) || isMongolianSelector(previousPoint)) && isMongolianLetterOrMark(_optionalChain([points, 'access',
|
|
691
|
+
return (isMongolianLetterOrMark(previousPoint) || isMongolianSelector(previousPoint)) && isMongolianLetterOrMark(_optionalChain([points, 'access', _12 => _12[index + 1], 'optionalAccess', _13 => _13.codePointAt, 'call', _14 => _14(0)]));
|
|
529
692
|
}
|
|
530
693
|
return isMongolianLetterOrMark(previousPoint);
|
|
531
694
|
}
|
|
@@ -690,14 +853,14 @@ function containsPhrase(text, phrase) {
|
|
|
690
853
|
return false;
|
|
691
854
|
}
|
|
692
855
|
function compileRules(tolerance) {
|
|
693
|
-
const equivalences = _optionalChain([tolerance, 'optionalAccess',
|
|
856
|
+
const equivalences = _optionalChain([tolerance, 'optionalAccess', _15 => _15.equivalences]);
|
|
694
857
|
if (!Array.isArray(equivalences)) {
|
|
695
858
|
return { rules: [], signature: "[]" };
|
|
696
859
|
}
|
|
697
860
|
const rules = [];
|
|
698
861
|
for (const rule of equivalences.slice(0, DICTATION_MAX_EQUIVALENCES)) {
|
|
699
|
-
const rawFrom = _optionalChain([rule, 'optionalAccess',
|
|
700
|
-
const rawTo = _optionalChain([rule, 'optionalAccess',
|
|
862
|
+
const rawFrom = _optionalChain([rule, 'optionalAccess', _16 => _16.from]);
|
|
863
|
+
const rawTo = _optionalChain([rule, 'optionalAccess', _17 => _17.to]);
|
|
701
864
|
if (typeof rawFrom !== "string" || typeof rawTo !== "string" || isLongerThan(rawFrom, DICTATION_MAX_EQUIVALENCE_LENGTH) || isLongerThan(rawTo, DICTATION_MAX_EQUIVALENCE_LENGTH)) {
|
|
702
865
|
continue;
|
|
703
866
|
}
|
|
@@ -766,10 +929,10 @@ function scopedMap(key, signature) {
|
|
|
766
929
|
var PRE_STRIP_TEXTS = /* @__PURE__ */ Symbol("dictation pre-strip texts");
|
|
767
930
|
function boundedPreStrip(text) {
|
|
768
931
|
const memo = scopeCache(PRE_STRIP_TEXTS, () => /* @__PURE__ */ new Map());
|
|
769
|
-
let result = _optionalChain([memo, 'optionalAccess',
|
|
932
|
+
let result = _optionalChain([memo, 'optionalAccess', _18 => _18.get, 'call', _19 => _19(text)]);
|
|
770
933
|
if (result === void 0) {
|
|
771
934
|
result = bounded(preStripNormalize(text));
|
|
772
|
-
_optionalChain([memo, 'optionalAccess',
|
|
935
|
+
_optionalChain([memo, 'optionalAccess', _20 => _20.set, 'call', _21 => _21(text, result)]);
|
|
773
936
|
}
|
|
774
937
|
return result;
|
|
775
938
|
}
|
|
@@ -781,8 +944,8 @@ function workingText(text, compiled, preStripped = boundedPreStrip(text)) {
|
|
|
781
944
|
return preStripped;
|
|
782
945
|
}
|
|
783
946
|
const memo = scopeCache(WORKING_TEXTS, () => /* @__PURE__ */ new Map());
|
|
784
|
-
let known = _optionalChain([memo, 'optionalAccess',
|
|
785
|
-
const remembered = _optionalChain([known, 'optionalAccess',
|
|
947
|
+
let known = _optionalChain([memo, 'optionalAccess', _22 => _22.get, 'call', _23 => _23(compiled.signature)]);
|
|
948
|
+
const remembered = _optionalChain([known, 'optionalAccess', _24 => _24.get, 'call', _25 => _25(text)]);
|
|
786
949
|
if (remembered !== void 0) {
|
|
787
950
|
return remembered;
|
|
788
951
|
}
|
|
@@ -820,7 +983,7 @@ function readingOf(written, working) {
|
|
|
820
983
|
if (source === "") {
|
|
821
984
|
return NOTHING_TO_REVEAL;
|
|
822
985
|
}
|
|
823
|
-
const long = (_nullishCoalesce(_optionalChain([source, 'access',
|
|
986
|
+
const long = (_nullishCoalesce(_optionalChain([source, 'access', _26 => _26.match, 'call', _27 => _27(UNSPACED_LETTERS_RE), 'optionalAccess', _28 => _28.length]), () => ( 0))) >= UNSPACED_REVEAL_MIN_CHARACTERS;
|
|
824
987
|
const { before, after } = phraseOf(source, long ? "none" : "visible");
|
|
825
988
|
const forms = [
|
|
826
989
|
writtenStripped,
|
|
@@ -883,7 +1046,7 @@ function dictationNormalizer(tolerance) {
|
|
|
883
1046
|
}
|
|
884
1047
|
function revealsCandidate(forms, candidate) {
|
|
885
1048
|
const memo = scopeCache(REVEALS, () => /* @__PURE__ */ new WeakMap());
|
|
886
|
-
const known = _optionalChain([memo, 'optionalAccess',
|
|
1049
|
+
const known = _optionalChain([memo, 'optionalAccess', _29 => _29.get, 'call', _30 => _30(forms), 'optionalAccess', _31 => _31.get, 'call', _32 => _32(candidate)]);
|
|
887
1050
|
if (known !== void 0) {
|
|
888
1051
|
return known;
|
|
889
1052
|
}
|
|
@@ -912,137 +1075,236 @@ function searchReveal(forms, candidate) {
|
|
|
912
1075
|
});
|
|
913
1076
|
}
|
|
914
1077
|
|
|
915
|
-
// src/
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
correct: _v4.z.string().min(1).optional(),
|
|
919
|
-
incorrect: _v4.z.string().min(1).optional()
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
// src/schemas/media.ts
|
|
923
|
-
|
|
924
|
-
var MediaUrlSchema = _v4.z.union([
|
|
925
|
-
_v4.z.url().refine((value) => /^(https?|data|blob):/i.test(value), {
|
|
926
|
-
error: "Absolute media URLs must use the https:, http:, data:, or blob: scheme."
|
|
927
|
-
}),
|
|
928
|
-
_v4.z.string().regex(/^\/(?!\/)\S*$/, {
|
|
929
|
-
error: 'Relative media URLs must be root-relative (a single leading "/").'
|
|
930
|
-
})
|
|
931
|
-
]);
|
|
932
|
-
var NativeControlHintSchema = _v4.z.enum(["hide-download", "hide-rate"]);
|
|
933
|
-
var MediaPlaybackSchema = _v4.z.strictObject({
|
|
934
|
-
/**
|
|
935
|
-
* `native` (the resolved default) renders today's `<audio controls>`
|
|
936
|
-
* unchanged. `minimal` — resolved automatically whenever any enforcement
|
|
937
|
-
* field is set — replaces the browser bar with the SDK transport:
|
|
938
|
-
* play/pause, elapsed/total, mute, volume, optional speed, optional
|
|
939
|
-
* scrubber, and a live plays-remaining status.
|
|
940
|
-
*/
|
|
941
|
-
controls: _v4.z.enum(["native", "minimal"]).optional(),
|
|
942
|
-
/**
|
|
943
|
-
* How many times the recording may be STARTED. A play is consumed when
|
|
944
|
-
* playback begins from anywhere other than where it last stopped, so
|
|
945
|
-
* pausing, resuming, and paging between the questions of one listening
|
|
946
|
-
* group are all free. Enforced in `practice` and `exam`; never in `review`.
|
|
947
|
-
*/
|
|
948
|
-
maxPlays: _v4.z.number().int().min(1).max(20).optional(),
|
|
949
|
-
/** `none` renders no scrubber and reverts an out-of-band seek to the high-water mark. */
|
|
950
|
-
seek: _v4.z.enum(["allow", "none"]).optional(),
|
|
951
|
-
/** `fixed` renders no speed control and snaps `playbackRate` back to 1. */
|
|
952
|
-
rate: _v4.z.enum(["allow", "fixed"]).optional(),
|
|
953
|
-
/** Advisory only. See {@link NativeControlHintSchema}. */
|
|
954
|
-
nativeControlHints: _v4.z.array(NativeControlHintSchema).min(1).max(2).optional()
|
|
955
|
-
});
|
|
956
|
-
var MediaTrackSchema = _v4.z.strictObject({
|
|
957
|
-
kind: _v4.z.enum(["captions", "subtitles"]),
|
|
958
|
-
src: MediaUrlSchema,
|
|
959
|
-
srclang: _v4.z.string().regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, {
|
|
960
|
-
error: 'srclang must be a BCP 47 language tag, such as "en", "es" or "pt-BR".'
|
|
961
|
-
}),
|
|
962
|
-
label: _v4.z.string().max(60).refine((label) => label.trim().length > 0, { error: "A track label must not be empty." }),
|
|
963
|
-
default: _v4.z.boolean().optional()
|
|
964
|
-
});
|
|
965
|
-
var MAX_TRACKS = 12;
|
|
966
|
-
function posterFits(m) {
|
|
967
|
-
return m.poster === void 0 || m.type === "video";
|
|
1078
|
+
// src/scoring/dictation/edit-distance.ts
|
|
1079
|
+
function codePoints(text) {
|
|
1080
|
+
return Array.from(text, (character) => character.codePointAt(0));
|
|
968
1081
|
}
|
|
969
|
-
function
|
|
970
|
-
|
|
1082
|
+
function editDistance(a, b) {
|
|
1083
|
+
if (a === b) {
|
|
1084
|
+
return 0;
|
|
1085
|
+
}
|
|
1086
|
+
let left = codePoints(a);
|
|
1087
|
+
let right = codePoints(b);
|
|
1088
|
+
if (left.length < right.length) {
|
|
1089
|
+
[left, right] = [right, left];
|
|
1090
|
+
}
|
|
1091
|
+
if (right.length === 0) {
|
|
1092
|
+
return left.length;
|
|
1093
|
+
}
|
|
1094
|
+
let previous = new Uint32Array(right.length + 1);
|
|
1095
|
+
let current = new Uint32Array(right.length + 1);
|
|
1096
|
+
for (let j = 0; j <= right.length; j += 1) {
|
|
1097
|
+
previous[j] = j;
|
|
1098
|
+
}
|
|
1099
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
1100
|
+
current[0] = i;
|
|
1101
|
+
const leftPoint = left[i - 1];
|
|
1102
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
1103
|
+
const deletion = previous[j] + 1;
|
|
1104
|
+
const insertion = current[j - 1] + 1;
|
|
1105
|
+
const substitution = previous[j - 1] + (leftPoint === right[j - 1] ? 0 : 1);
|
|
1106
|
+
current[j] = Math.min(deletion, insertion, substitution);
|
|
1107
|
+
}
|
|
1108
|
+
[previous, current] = [current, previous];
|
|
1109
|
+
}
|
|
1110
|
+
return previous[right.length];
|
|
971
1111
|
}
|
|
972
|
-
function
|
|
973
|
-
|
|
974
|
-
|
|
1112
|
+
function similarity(a, b) {
|
|
1113
|
+
const max = Math.max(codePoints(a).length, codePoints(b).length);
|
|
1114
|
+
return max === 0 ? 0 : (max - editDistance(a, b)) / max;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/scoring/dictation/align.ts
|
|
1118
|
+
function dictationReferenceWords(data) {
|
|
1119
|
+
return rawCandidates(data).map(
|
|
1120
|
+
(candidate) => tokensOf(normalizeCandidate(candidate, data)).map((word, index) => ({
|
|
1121
|
+
itemId: `w${index + 1}`,
|
|
1122
|
+
word
|
|
1123
|
+
}))
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
function alignDictation(data, text) {
|
|
1127
|
+
const raw = typeof text === "string" ? text : "";
|
|
1128
|
+
const capped = truncateCodePoints(raw, DICTATION_MAX_TEXT_LENGTH);
|
|
1129
|
+
const { text: normalized, cut } = normalizeDictationTextBounded(capped, data.tolerance);
|
|
1130
|
+
const attempt = cutAt(normalized, DICTATION_MAX_TEXT_LENGTH);
|
|
1131
|
+
const truncated = cut || capped.length < raw.length || attempt.length < normalized.length;
|
|
1132
|
+
const candidates = [];
|
|
1133
|
+
rawCandidates(data).forEach((candidate, index) => {
|
|
1134
|
+
const candidateText = normalizeCandidate(candidate, data);
|
|
1135
|
+
if (candidateText !== "") {
|
|
1136
|
+
candidates.push({ index, text: candidateText });
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
if (candidates.length === 0) {
|
|
1140
|
+
return { candidateIndex: -1, reference: "", attempt, truncated, similarity: 0, words: [] };
|
|
975
1141
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1142
|
+
let chosen = candidates[0];
|
|
1143
|
+
let best = similarity(chosen.text, attempt);
|
|
1144
|
+
for (const candidate of candidates.slice(1)) {
|
|
1145
|
+
const candidateSimilarity = similarity(candidate.text, attempt);
|
|
1146
|
+
if (candidateSimilarity > best) {
|
|
1147
|
+
best = candidateSimilarity;
|
|
1148
|
+
chosen = candidate;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
candidateIndex: chosen.index,
|
|
1153
|
+
reference: chosen.text,
|
|
1154
|
+
attempt,
|
|
1155
|
+
truncated,
|
|
1156
|
+
similarity: best,
|
|
1157
|
+
words: alignWords(tokensOf(chosen.text), tokensOf(attempt))
|
|
1158
|
+
};
|
|
979
1159
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1160
|
+
function rawCandidates(data) {
|
|
1161
|
+
return [
|
|
1162
|
+
data.transcript,
|
|
1163
|
+
...Array.isArray(data.acceptedTranscripts) ? data.acceptedTranscripts.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS) : []
|
|
1164
|
+
];
|
|
1165
|
+
}
|
|
1166
|
+
function normalizeCandidate(candidate, data) {
|
|
1167
|
+
return typeof candidate === "string" ? cutAt(
|
|
1168
|
+
normalizeDictationText(
|
|
1169
|
+
truncateCodePoints(candidate, DICTATION_MAX_TRANSCRIPT_LENGTH),
|
|
1170
|
+
data.tolerance
|
|
1171
|
+
),
|
|
1172
|
+
DICTATION_MAX_TRANSCRIPT_LENGTH
|
|
1173
|
+
) : "";
|
|
1174
|
+
}
|
|
1175
|
+
function cutAt(normalized, max) {
|
|
1176
|
+
const cut = truncateCodePoints(normalized, max);
|
|
1177
|
+
return cut.endsWith(" ") ? cut.slice(0, -1) : cut;
|
|
1178
|
+
}
|
|
1179
|
+
function tokensOf(normalized) {
|
|
1180
|
+
return normalized === "" ? [] : normalized.split(" ");
|
|
1181
|
+
}
|
|
1182
|
+
var STEP = {
|
|
1183
|
+
/** A reference element paired with an attempt element — equal or substituted. */
|
|
1184
|
+
pair: 0,
|
|
1185
|
+
/** A reference element the attempt lacks. */
|
|
1186
|
+
missing: 1,
|
|
1187
|
+
/** An attempt element the reference lacks. */
|
|
1188
|
+
extra: 2
|
|
985
1189
|
};
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
{
|
|
997
|
-
error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
|
|
998
|
-
path: ["alt"]
|
|
1190
|
+
function alignSequences(reference, attempt) {
|
|
1191
|
+
const rows = reference.length;
|
|
1192
|
+
const columns = attempt.length;
|
|
1193
|
+
const width = columns + 1;
|
|
1194
|
+
const steps = new Uint8Array((rows + 1) * width);
|
|
1195
|
+
let previous = new Uint32Array(width);
|
|
1196
|
+
let current = new Uint32Array(width);
|
|
1197
|
+
for (let j2 = 0; j2 <= columns; j2 += 1) {
|
|
1198
|
+
previous[j2] = j2;
|
|
1199
|
+
steps[j2] = STEP.extra;
|
|
999
1200
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1201
|
+
for (let i2 = 1; i2 <= rows; i2 += 1) {
|
|
1202
|
+
current[0] = i2;
|
|
1203
|
+
steps[i2 * width] = STEP.missing;
|
|
1204
|
+
const referenceItem = reference[i2 - 1];
|
|
1205
|
+
for (let j2 = 1; j2 <= columns; j2 += 1) {
|
|
1206
|
+
const paired = previous[j2 - 1] + (referenceItem === attempt[j2 - 1] ? 0 : 1);
|
|
1207
|
+
const missing = previous[j2] + 1;
|
|
1208
|
+
const extra = current[j2 - 1] + 1;
|
|
1209
|
+
const cost = Math.min(paired, missing, extra);
|
|
1210
|
+
current[j2] = cost;
|
|
1211
|
+
steps[i2 * width + j2] = paired === cost ? STEP.pair : missing === cost ? STEP.missing : STEP.extra;
|
|
1212
|
+
}
|
|
1213
|
+
[previous, current] = [current, previous];
|
|
1011
1214
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1215
|
+
const path = [];
|
|
1216
|
+
let i = rows;
|
|
1217
|
+
let j = columns;
|
|
1218
|
+
while (i > 0 || j > 0) {
|
|
1219
|
+
const step = steps[i * width + j];
|
|
1220
|
+
if (step === STEP.pair) {
|
|
1221
|
+
i -= 1;
|
|
1222
|
+
j -= 1;
|
|
1223
|
+
} else if (step === STEP.missing) {
|
|
1224
|
+
i -= 1;
|
|
1225
|
+
} else {
|
|
1226
|
+
j -= 1;
|
|
1227
|
+
}
|
|
1228
|
+
path.push({ step, referenceIndex: i, attemptIndex: j });
|
|
1020
1229
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1230
|
+
return path.reverse();
|
|
1231
|
+
}
|
|
1232
|
+
function alignWords(reference, attempt) {
|
|
1233
|
+
const words = [];
|
|
1234
|
+
let ordinal = 0;
|
|
1235
|
+
for (const { step, referenceIndex, attemptIndex } of alignSequences(reference, attempt)) {
|
|
1236
|
+
if (step === STEP.extra) {
|
|
1237
|
+
words.push({
|
|
1238
|
+
reference: "",
|
|
1239
|
+
attempt: attempt[attemptIndex],
|
|
1240
|
+
similarity: 0,
|
|
1241
|
+
status: "extra"
|
|
1242
|
+
});
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
ordinal += 1;
|
|
1246
|
+
const referenceWord = reference[referenceIndex];
|
|
1247
|
+
if (step === STEP.missing) {
|
|
1248
|
+
words.push({
|
|
1249
|
+
itemId: `w${ordinal}`,
|
|
1250
|
+
reference: referenceWord,
|
|
1251
|
+
attempt: "",
|
|
1252
|
+
similarity: 0,
|
|
1253
|
+
status: "missing"
|
|
1254
|
+
});
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const attemptWord = attempt[attemptIndex];
|
|
1258
|
+
words.push({
|
|
1259
|
+
itemId: `w${ordinal}`,
|
|
1260
|
+
reference: referenceWord,
|
|
1261
|
+
attempt: attemptWord,
|
|
1262
|
+
similarity: similarity(referenceWord, attemptWord),
|
|
1263
|
+
status: referenceWord === attemptWord ? "correct" : "incorrect"
|
|
1264
|
+
});
|
|
1026
1265
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
(m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
|
|
1038
|
-
{
|
|
1039
|
-
error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
|
|
1040
|
-
path: ["alt"]
|
|
1266
|
+
return words;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/errors.ts
|
|
1270
|
+
var ActivitySchemaError = class extends Error {
|
|
1271
|
+
constructor(activityType, errors) {
|
|
1272
|
+
super(`Invalid activity data for type "${activityType}"`);
|
|
1273
|
+
this.activityType = activityType;
|
|
1274
|
+
this.errors = errors;
|
|
1275
|
+
this.name = "ActivitySchemaError";
|
|
1041
1276
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
};
|
|
1280
|
+
var UnknownActivityTypeError = class extends Error {
|
|
1281
|
+
constructor(activityType) {
|
|
1282
|
+
super(`Activity type "${activityType}" is not registered`);
|
|
1283
|
+
this.activityType = activityType;
|
|
1284
|
+
this.name = "UnknownActivityTypeError";
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
};
|
|
1288
|
+
var RedactedScoringError = class extends Error {
|
|
1289
|
+
constructor(activityType) {
|
|
1290
|
+
super(
|
|
1291
|
+
`Activity data for "${activityType}" carries no answer key (it looks redacted), so it cannot be scored. Score against the full activity data server-side. evaluate() answers this input without throwing, but it never grades it.`
|
|
1292
|
+
);
|
|
1293
|
+
this.activityType = activityType;
|
|
1294
|
+
this.name = "RedactedScoringError";
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
};
|
|
1298
|
+
var DeferredScoringError = class extends Error {
|
|
1299
|
+
constructor(activityType) {
|
|
1300
|
+
super(
|
|
1301
|
+
`Activity type "${activityType}" is graded asynchronously and has no synchronous score. Use evaluate() \u2014 it returns { status: 'deferred' } for this type.`
|
|
1302
|
+
);
|
|
1303
|
+
this.activityType = activityType;
|
|
1304
|
+
this.name = "DeferredScoringError";
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
};
|
|
1046
1308
|
|
|
1047
1309
|
// src/schemas/dictation.ts
|
|
1048
1310
|
|
|
@@ -1106,7 +1368,8 @@ var DictationDataShape = _v4.z.looseObject({
|
|
|
1106
1368
|
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
1107
1369
|
locale: _v4.z.string().optional(),
|
|
1108
1370
|
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
1109
|
-
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
1371
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
|
|
1372
|
+
ai: AiPermissionsSchema.optional()
|
|
1110
1373
|
});
|
|
1111
1374
|
var DictationDataSchema = DictationDataShape.check(
|
|
1112
1375
|
checkEvenAfterIssues((ctx) => {
|
|
@@ -1208,10 +1471,10 @@ var DictationDataSchema = DictationDataShape.check(
|
|
|
1208
1471
|
}
|
|
1209
1472
|
for (const field of ["media", "slowMedia"]) {
|
|
1210
1473
|
const recording = data[field];
|
|
1211
|
-
|
|
1474
|
+
for (const captions of captionFieldsOf(recording)) {
|
|
1212
1475
|
report(
|
|
1213
|
-
[field,
|
|
1214
|
-
recording
|
|
1476
|
+
[field, captions],
|
|
1477
|
+
_optionalChain([recording, 'optionalAccess', _48 => _48[captions]]),
|
|
1215
1478
|
"A dictation recording cannot carry captions: the captions are the answer. Offer an accessible alternative as a different item."
|
|
1216
1479
|
);
|
|
1217
1480
|
}
|
|
@@ -1219,110 +1482,62 @@ var DictationDataSchema = DictationDataShape.check(
|
|
|
1219
1482
|
if (!rulesParse || !transcriptParses) {
|
|
1220
1483
|
return;
|
|
1221
1484
|
}
|
|
1222
|
-
const normalizer = normalizerFor(data);
|
|
1223
|
-
const candidates = candidatesOf(data).map((entry) => normalizer.measure(entry));
|
|
1224
|
-
const shown = [
|
|
1225
|
-
[["title"], refused.has("title") ? void 0 : data.title],
|
|
1226
|
-
[["media", "alt"], _optionalChain([data, 'access', _49 => _49.media, 'optionalAccess', _50 => _50.alt])],
|
|
1227
|
-
[["slowMedia", "alt"], _optionalChain([data, 'access', _51 => _51.slowMedia, 'optionalAccess', _52 => _52.alt])]
|
|
1228
|
-
];
|
|
1229
|
-
for (const [path, value] of shown) {
|
|
1230
|
-
if (value === void 0) {
|
|
1231
|
-
continue;
|
|
1232
|
-
}
|
|
1233
|
-
const { tooLong, forms } = normalizer.shown(value);
|
|
1234
|
-
if (tooLong) {
|
|
1235
|
-
report(
|
|
1236
|
-
path,
|
|
1237
|
-
value,
|
|
1238
|
-
`The title and the recording descriptions are searched for the transcript before they are shown: each is at most ${WORKING_LENGTH} characters, before and after the equivalences are applied, and the equivalences may not grow it past that on the way.`
|
|
1239
|
-
);
|
|
1240
|
-
} else if (candidates.some((candidate) => revealsCandidate(forms, candidate))) {
|
|
1241
|
-
report(
|
|
1242
|
-
path,
|
|
1243
|
-
value,
|
|
1244
|
-
'The title and the recording descriptions are shown to the learner before they type: they must not contain the transcript. Describe the recording ("Recording, normal speed"), never transcribe it.'
|
|
1245
|
-
);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
})
|
|
1249
|
-
);
|
|
1250
|
-
function groupCaptionsRevealDictation(group, hasCaptions = (captionsUrl) => captionsUrl !== void 0) {
|
|
1251
|
-
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1252
|
-
if (!isObject(group) || !isObject(group.stimulus) || !isObject(group.stimulus.media)) {
|
|
1253
|
-
return false;
|
|
1254
|
-
}
|
|
1255
|
-
if (!hasCaptions(group.stimulus.media.captionsUrl) || !Array.isArray(group.items)) {
|
|
1256
|
-
return false;
|
|
1257
|
-
}
|
|
1258
|
-
return group.items.some(
|
|
1259
|
-
(item) => isObject(item) && item.type === "dictation" && item.media === void 0
|
|
1260
|
-
);
|
|
1261
|
-
}
|
|
1262
|
-
var GROUP_CAPTIONS_REVEAL_DICTATION = "The stimulus recording carries captions, and a dictation in this group plays that recording: the captions are the answer. Remove the captions, or give the dictation its own recording.";
|
|
1263
|
-
|
|
1264
|
-
// src/schemas/fill-in-the-blanks.ts
|
|
1265
|
-
|
|
1266
|
-
var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
1267
|
-
var TextMatchPolicySchema = _v4.z.looseObject({
|
|
1268
|
-
caseSensitive: _v4.z.boolean().optional(),
|
|
1269
|
-
trim: _v4.z.boolean().optional(),
|
|
1270
|
-
normalize: _v4.z.enum(["none", "NFC", "NFKC"]).optional(),
|
|
1271
|
-
foldDiacritics: _v4.z.boolean().optional(),
|
|
1272
|
-
collapseInnerWhitespace: _v4.z.boolean().optional(),
|
|
1273
|
-
ignorePunctuation: _v4.z.boolean().optional(),
|
|
1274
|
-
levenshtein: _v4.z.number().int().min(0).optional(),
|
|
1275
|
-
locale: _v4.z.string().optional()
|
|
1276
|
-
});
|
|
1277
|
-
var BlankConfigSchema = _v4.z.looseObject({
|
|
1278
|
-
id: _v4.z.string().min(1),
|
|
1279
|
-
acceptedAnswers: _v4.z.array(
|
|
1280
|
-
_v4.z.string().min(1).refine((answer) => answer.trim().length > 0, {
|
|
1281
|
-
error: "Accepted answers must contain non-whitespace characters."
|
|
1282
|
-
})
|
|
1283
|
-
).min(1),
|
|
1284
|
-
caseSensitive: _v4.z.boolean().optional(),
|
|
1285
|
-
trimWhitespace: _v4.z.boolean().optional(),
|
|
1286
|
-
match: TextMatchPolicySchema.optional(),
|
|
1287
|
-
hint: _v4.z.string().optional(),
|
|
1288
|
-
feedback: _v4.z.string().optional()
|
|
1289
|
-
});
|
|
1290
|
-
var FillInTheBlanksDataSchema = _v4.z.looseObject({
|
|
1291
|
-
schemaVersion: _v4.z.literal("1.0"),
|
|
1292
|
-
type: _v4.z.literal("fill-in-the-blanks"),
|
|
1293
|
-
id: _v4.z.string().min(1),
|
|
1294
|
-
title: _v4.z.string().min(1),
|
|
1295
|
-
passage: _v4.z.string().min(1),
|
|
1296
|
-
passageHtml: _v4.z.string().optional(),
|
|
1297
|
-
blanks: _v4.z.array(BlankConfigSchema).min(1),
|
|
1298
|
-
scoringStrategy: _v4.z.enum(["all-or-nothing", "partial"]),
|
|
1299
|
-
media: MediaSchema.optional(),
|
|
1300
|
-
feedback: FeedbackSchema.optional(),
|
|
1301
|
-
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
1302
|
-
locale: _v4.z.string().optional(),
|
|
1303
|
-
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
1304
|
-
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
1305
|
-
}).refine(
|
|
1306
|
-
(data) => {
|
|
1307
|
-
const placeholderCounts2 = /* @__PURE__ */ new Map();
|
|
1308
|
-
for (const match of data.passage.matchAll(PLACEHOLDER_RE)) {
|
|
1309
|
-
const id = match[1];
|
|
1310
|
-
placeholderCounts2.set(id, (_nullishCoalesce(placeholderCounts2.get(id), () => ( 0))) + 1);
|
|
1311
|
-
}
|
|
1312
|
-
const blankIds = data.blanks.map((blank) => blank.id);
|
|
1313
|
-
if (new Set(blankIds).size !== blankIds.length) {
|
|
1314
|
-
return false;
|
|
1315
|
-
}
|
|
1316
|
-
if (placeholderCounts2.size !== blankIds.length) {
|
|
1317
|
-
return false;
|
|
1318
|
-
}
|
|
1319
|
-
return blankIds.every((id) => placeholderCounts2.get(id) === 1);
|
|
1320
|
-
},
|
|
1321
|
-
{
|
|
1322
|
-
error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
|
|
1323
|
-
path: ["passage"]
|
|
1324
|
-
}
|
|
1485
|
+
const normalizer = normalizerFor(data);
|
|
1486
|
+
const candidates = candidatesOf(data).map((entry) => normalizer.measure(entry));
|
|
1487
|
+
const shown = [
|
|
1488
|
+
[["title"], refused.has("title") ? void 0 : data.title],
|
|
1489
|
+
[["media", "alt"], _optionalChain([data, 'access', _49 => _49.media, 'optionalAccess', _50 => _50.alt])],
|
|
1490
|
+
[["slowMedia", "alt"], _optionalChain([data, 'access', _51 => _51.slowMedia, 'optionalAccess', _52 => _52.alt])]
|
|
1491
|
+
];
|
|
1492
|
+
for (const [path, value] of shown) {
|
|
1493
|
+
if (value === void 0) {
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
const { tooLong, forms } = normalizer.shown(value);
|
|
1497
|
+
if (tooLong) {
|
|
1498
|
+
report(
|
|
1499
|
+
path,
|
|
1500
|
+
value,
|
|
1501
|
+
`The title and the recording descriptions are searched for the transcript before they are shown: each is at most ${WORKING_LENGTH} characters, before and after the equivalences are applied, and the equivalences may not grow it past that on the way.`
|
|
1502
|
+
);
|
|
1503
|
+
} else if (candidates.some((candidate) => revealsCandidate(forms, candidate))) {
|
|
1504
|
+
report(
|
|
1505
|
+
path,
|
|
1506
|
+
value,
|
|
1507
|
+
'The title and the recording descriptions are shown to the learner before they type: they must not contain the transcript. Describe the recording ("Recording, normal speed"), never transcribe it.'
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
})
|
|
1325
1512
|
);
|
|
1513
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1514
|
+
function captionFieldsOf(media, written = (captionsUrl) => captionsUrl !== void 0) {
|
|
1515
|
+
if (!isObject(media)) {
|
|
1516
|
+
return [];
|
|
1517
|
+
}
|
|
1518
|
+
const fields = [];
|
|
1519
|
+
if (written(media.captionsUrl)) {
|
|
1520
|
+
fields.push("captionsUrl");
|
|
1521
|
+
}
|
|
1522
|
+
if (Array.isArray(media.tracks) && media.tracks.length > 0) {
|
|
1523
|
+
fields.push("tracks");
|
|
1524
|
+
}
|
|
1525
|
+
return fields;
|
|
1526
|
+
}
|
|
1527
|
+
function groupCaptionsField(group, written) {
|
|
1528
|
+
if (!isObject(group) || !isObject(group.stimulus) || !Array.isArray(group.items)) {
|
|
1529
|
+
return void 0;
|
|
1530
|
+
}
|
|
1531
|
+
const [field] = captionFieldsOf(group.stimulus.media, written);
|
|
1532
|
+
if (field === void 0) {
|
|
1533
|
+
return void 0;
|
|
1534
|
+
}
|
|
1535
|
+
const played = group.items.some(
|
|
1536
|
+
(item) => isObject(item) && item.type === "dictation" && item.media === void 0
|
|
1537
|
+
);
|
|
1538
|
+
return played ? field : void 0;
|
|
1539
|
+
}
|
|
1540
|
+
var GROUP_CAPTIONS_REVEAL_DICTATION = "The stimulus recording carries captions, and a dictation in this group plays that recording: the captions are the answer. Remove the captions, or give the dictation its own recording.";
|
|
1326
1541
|
|
|
1327
1542
|
// src/schemas/multiple-choice.ts
|
|
1328
1543
|
|
|
@@ -1364,7 +1579,8 @@ var MultipleChoiceDataSchema = _v4.z.looseObject({
|
|
|
1364
1579
|
shuffle: _v4.z.boolean().optional(),
|
|
1365
1580
|
locale: _v4.z.string().optional(),
|
|
1366
1581
|
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
1367
|
-
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
1582
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
|
|
1583
|
+
ai: AiPermissionsSchema.optional()
|
|
1368
1584
|
}).refine((data) => data.options.some((option) => option.isCorrect), {
|
|
1369
1585
|
error: "At least one option must be marked correct.",
|
|
1370
1586
|
path: ["options"]
|
|
@@ -1438,7 +1654,8 @@ var ReadAloudDataShape = _v4.z.looseObject({
|
|
|
1438
1654
|
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
1439
1655
|
feedback: FeedbackSchema.optional(),
|
|
1440
1656
|
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
1441
|
-
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
1657
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
|
|
1658
|
+
ai: AiPermissionsSchema.optional()
|
|
1442
1659
|
});
|
|
1443
1660
|
var ReadAloudDataSchema = ReadAloudDataShape.check(
|
|
1444
1661
|
checkEvenAfterIssues2((ctx) => {
|
|
@@ -1604,7 +1821,8 @@ var GapSelectDataSchema = _v4.z.looseObject({
|
|
|
1604
1821
|
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
1605
1822
|
locale: _v4.z.string().optional(),
|
|
1606
1823
|
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
1607
|
-
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
1824
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
|
|
1825
|
+
ai: AiPermissionsSchema.optional()
|
|
1608
1826
|
}).refine(
|
|
1609
1827
|
(data) => {
|
|
1610
1828
|
const ids = (_nullishCoalesce(data.banks, () => ( []))).map((bank) => bank.id);
|
|
@@ -1693,7 +1911,8 @@ var WrittenResponseDataSchema = _v4.z.looseObject({
|
|
|
1693
1911
|
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
1694
1912
|
locale: _v4.z.string().optional(),
|
|
1695
1913
|
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
1696
|
-
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
1914
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
|
|
1915
|
+
ai: AiPermissionsSchema.optional()
|
|
1697
1916
|
}).refine((data) => data.maxWords >= data.minWords, {
|
|
1698
1917
|
error: "maxWords must be greater than or equal to minWords.",
|
|
1699
1918
|
path: ["maxWords"]
|
|
@@ -1713,7 +1932,8 @@ var redactedBase = {
|
|
|
1713
1932
|
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
1714
1933
|
locale: _v4.z.string().optional(),
|
|
1715
1934
|
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
1716
|
-
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
1935
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
|
|
1936
|
+
ai: _v4.z.strictObject({ explanations: _v4.z.boolean().optional(), hints: _v4.z.boolean().optional() }).optional()
|
|
1717
1937
|
};
|
|
1718
1938
|
var RedactedMultipleChoiceOptionMediaSchema = _v4.z.strictObject({
|
|
1719
1939
|
type: _v4.z.enum(["image", "audio"]),
|
|
@@ -1791,12 +2011,12 @@ var RedactedDictationDataSchema = _v4.z.strictObject({
|
|
|
1791
2011
|
hints: _v4.z.strictObject({ mode: _v4.z.literal("progressive-words") }).optional()
|
|
1792
2012
|
}).check((ctx) => {
|
|
1793
2013
|
const data = ctx.value;
|
|
1794
|
-
|
|
2014
|
+
for (const captions of captionFieldsOf(data.media)) {
|
|
1795
2015
|
ctx.issues.push({
|
|
1796
2016
|
code: "custom",
|
|
1797
|
-
input: data.media
|
|
2017
|
+
input: _optionalChain([data, 'access', _65 => _65.media, 'optionalAccess', _66 => _66[captions]]),
|
|
1798
2018
|
message: "A dictation recording cannot carry captions: the captions are the answer, so this payload is not learner-safe.",
|
|
1799
|
-
path: ["media",
|
|
2019
|
+
path: ["media", captions]
|
|
1800
2020
|
});
|
|
1801
2021
|
}
|
|
1802
2022
|
if (data.slowMedia !== void 0 && _optionalChain([data, 'access', _67 => _67.media, 'optionalAccess', _68 => _68.playback, 'optionalAccess', _69 => _69.maxPlays]) !== void 0) {
|
|
@@ -1872,197 +2092,6 @@ var RedactedReadAloudDataSchema = _v4.z.strictObject({
|
|
|
1872
2092
|
}
|
|
1873
2093
|
});
|
|
1874
2094
|
|
|
1875
|
-
// src/scoring/dictation/edit-distance.ts
|
|
1876
|
-
function codePoints(text) {
|
|
1877
|
-
return Array.from(text, (character) => character.codePointAt(0));
|
|
1878
|
-
}
|
|
1879
|
-
function editDistance(a, b) {
|
|
1880
|
-
if (a === b) {
|
|
1881
|
-
return 0;
|
|
1882
|
-
}
|
|
1883
|
-
let left = codePoints(a);
|
|
1884
|
-
let right = codePoints(b);
|
|
1885
|
-
if (left.length < right.length) {
|
|
1886
|
-
[left, right] = [right, left];
|
|
1887
|
-
}
|
|
1888
|
-
if (right.length === 0) {
|
|
1889
|
-
return left.length;
|
|
1890
|
-
}
|
|
1891
|
-
let previous = new Uint32Array(right.length + 1);
|
|
1892
|
-
let current = new Uint32Array(right.length + 1);
|
|
1893
|
-
for (let j = 0; j <= right.length; j += 1) {
|
|
1894
|
-
previous[j] = j;
|
|
1895
|
-
}
|
|
1896
|
-
for (let i = 1; i <= left.length; i += 1) {
|
|
1897
|
-
current[0] = i;
|
|
1898
|
-
const leftPoint = left[i - 1];
|
|
1899
|
-
for (let j = 1; j <= right.length; j += 1) {
|
|
1900
|
-
const deletion = previous[j] + 1;
|
|
1901
|
-
const insertion = current[j - 1] + 1;
|
|
1902
|
-
const substitution = previous[j - 1] + (leftPoint === right[j - 1] ? 0 : 1);
|
|
1903
|
-
current[j] = Math.min(deletion, insertion, substitution);
|
|
1904
|
-
}
|
|
1905
|
-
[previous, current] = [current, previous];
|
|
1906
|
-
}
|
|
1907
|
-
return previous[right.length];
|
|
1908
|
-
}
|
|
1909
|
-
function similarity(a, b) {
|
|
1910
|
-
const max = Math.max(codePoints(a).length, codePoints(b).length);
|
|
1911
|
-
return max === 0 ? 0 : (max - editDistance(a, b)) / max;
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
// src/scoring/dictation/align.ts
|
|
1915
|
-
function dictationReferenceWords(data) {
|
|
1916
|
-
return rawCandidates(data).map(
|
|
1917
|
-
(candidate) => tokensOf(normalizeCandidate(candidate, data)).map((word, index) => ({
|
|
1918
|
-
itemId: `w${index + 1}`,
|
|
1919
|
-
word
|
|
1920
|
-
}))
|
|
1921
|
-
);
|
|
1922
|
-
}
|
|
1923
|
-
function alignDictation(data, text) {
|
|
1924
|
-
const raw = typeof text === "string" ? text : "";
|
|
1925
|
-
const capped = truncateCodePoints(raw, DICTATION_MAX_TEXT_LENGTH);
|
|
1926
|
-
const { text: normalized, cut } = normalizeDictationTextBounded(capped, data.tolerance);
|
|
1927
|
-
const attempt = cutAt(normalized, DICTATION_MAX_TEXT_LENGTH);
|
|
1928
|
-
const truncated = cut || capped.length < raw.length || attempt.length < normalized.length;
|
|
1929
|
-
const candidates = [];
|
|
1930
|
-
rawCandidates(data).forEach((candidate, index) => {
|
|
1931
|
-
const candidateText = normalizeCandidate(candidate, data);
|
|
1932
|
-
if (candidateText !== "") {
|
|
1933
|
-
candidates.push({ index, text: candidateText });
|
|
1934
|
-
}
|
|
1935
|
-
});
|
|
1936
|
-
if (candidates.length === 0) {
|
|
1937
|
-
return { candidateIndex: -1, reference: "", attempt, truncated, similarity: 0, words: [] };
|
|
1938
|
-
}
|
|
1939
|
-
let chosen = candidates[0];
|
|
1940
|
-
let best = similarity(chosen.text, attempt);
|
|
1941
|
-
for (const candidate of candidates.slice(1)) {
|
|
1942
|
-
const candidateSimilarity = similarity(candidate.text, attempt);
|
|
1943
|
-
if (candidateSimilarity > best) {
|
|
1944
|
-
best = candidateSimilarity;
|
|
1945
|
-
chosen = candidate;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
return {
|
|
1949
|
-
candidateIndex: chosen.index,
|
|
1950
|
-
reference: chosen.text,
|
|
1951
|
-
attempt,
|
|
1952
|
-
truncated,
|
|
1953
|
-
similarity: best,
|
|
1954
|
-
words: alignWords(tokensOf(chosen.text), tokensOf(attempt))
|
|
1955
|
-
};
|
|
1956
|
-
}
|
|
1957
|
-
function rawCandidates(data) {
|
|
1958
|
-
return [
|
|
1959
|
-
data.transcript,
|
|
1960
|
-
...Array.isArray(data.acceptedTranscripts) ? data.acceptedTranscripts.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS) : []
|
|
1961
|
-
];
|
|
1962
|
-
}
|
|
1963
|
-
function normalizeCandidate(candidate, data) {
|
|
1964
|
-
return typeof candidate === "string" ? cutAt(
|
|
1965
|
-
normalizeDictationText(
|
|
1966
|
-
truncateCodePoints(candidate, DICTATION_MAX_TRANSCRIPT_LENGTH),
|
|
1967
|
-
data.tolerance
|
|
1968
|
-
),
|
|
1969
|
-
DICTATION_MAX_TRANSCRIPT_LENGTH
|
|
1970
|
-
) : "";
|
|
1971
|
-
}
|
|
1972
|
-
function cutAt(normalized, max) {
|
|
1973
|
-
const cut = truncateCodePoints(normalized, max);
|
|
1974
|
-
return cut.endsWith(" ") ? cut.slice(0, -1) : cut;
|
|
1975
|
-
}
|
|
1976
|
-
function tokensOf(normalized) {
|
|
1977
|
-
return normalized === "" ? [] : normalized.split(" ");
|
|
1978
|
-
}
|
|
1979
|
-
var STEP = {
|
|
1980
|
-
/** A reference element paired with an attempt element — equal or substituted. */
|
|
1981
|
-
pair: 0,
|
|
1982
|
-
/** A reference element the attempt lacks. */
|
|
1983
|
-
missing: 1,
|
|
1984
|
-
/** An attempt element the reference lacks. */
|
|
1985
|
-
extra: 2
|
|
1986
|
-
};
|
|
1987
|
-
function alignSequences(reference, attempt) {
|
|
1988
|
-
const rows = reference.length;
|
|
1989
|
-
const columns = attempt.length;
|
|
1990
|
-
const width = columns + 1;
|
|
1991
|
-
const steps = new Uint8Array((rows + 1) * width);
|
|
1992
|
-
let previous = new Uint32Array(width);
|
|
1993
|
-
let current = new Uint32Array(width);
|
|
1994
|
-
for (let j2 = 0; j2 <= columns; j2 += 1) {
|
|
1995
|
-
previous[j2] = j2;
|
|
1996
|
-
steps[j2] = STEP.extra;
|
|
1997
|
-
}
|
|
1998
|
-
for (let i2 = 1; i2 <= rows; i2 += 1) {
|
|
1999
|
-
current[0] = i2;
|
|
2000
|
-
steps[i2 * width] = STEP.missing;
|
|
2001
|
-
const referenceItem = reference[i2 - 1];
|
|
2002
|
-
for (let j2 = 1; j2 <= columns; j2 += 1) {
|
|
2003
|
-
const paired = previous[j2 - 1] + (referenceItem === attempt[j2 - 1] ? 0 : 1);
|
|
2004
|
-
const missing = previous[j2] + 1;
|
|
2005
|
-
const extra = current[j2 - 1] + 1;
|
|
2006
|
-
const cost = Math.min(paired, missing, extra);
|
|
2007
|
-
current[j2] = cost;
|
|
2008
|
-
steps[i2 * width + j2] = paired === cost ? STEP.pair : missing === cost ? STEP.missing : STEP.extra;
|
|
2009
|
-
}
|
|
2010
|
-
[previous, current] = [current, previous];
|
|
2011
|
-
}
|
|
2012
|
-
const path = [];
|
|
2013
|
-
let i = rows;
|
|
2014
|
-
let j = columns;
|
|
2015
|
-
while (i > 0 || j > 0) {
|
|
2016
|
-
const step = steps[i * width + j];
|
|
2017
|
-
if (step === STEP.pair) {
|
|
2018
|
-
i -= 1;
|
|
2019
|
-
j -= 1;
|
|
2020
|
-
} else if (step === STEP.missing) {
|
|
2021
|
-
i -= 1;
|
|
2022
|
-
} else {
|
|
2023
|
-
j -= 1;
|
|
2024
|
-
}
|
|
2025
|
-
path.push({ step, referenceIndex: i, attemptIndex: j });
|
|
2026
|
-
}
|
|
2027
|
-
return path.reverse();
|
|
2028
|
-
}
|
|
2029
|
-
function alignWords(reference, attempt) {
|
|
2030
|
-
const words = [];
|
|
2031
|
-
let ordinal = 0;
|
|
2032
|
-
for (const { step, referenceIndex, attemptIndex } of alignSequences(reference, attempt)) {
|
|
2033
|
-
if (step === STEP.extra) {
|
|
2034
|
-
words.push({
|
|
2035
|
-
reference: "",
|
|
2036
|
-
attempt: attempt[attemptIndex],
|
|
2037
|
-
similarity: 0,
|
|
2038
|
-
status: "extra"
|
|
2039
|
-
});
|
|
2040
|
-
continue;
|
|
2041
|
-
}
|
|
2042
|
-
ordinal += 1;
|
|
2043
|
-
const referenceWord = reference[referenceIndex];
|
|
2044
|
-
if (step === STEP.missing) {
|
|
2045
|
-
words.push({
|
|
2046
|
-
itemId: `w${ordinal}`,
|
|
2047
|
-
reference: referenceWord,
|
|
2048
|
-
attempt: "",
|
|
2049
|
-
similarity: 0,
|
|
2050
|
-
status: "missing"
|
|
2051
|
-
});
|
|
2052
|
-
continue;
|
|
2053
|
-
}
|
|
2054
|
-
const attemptWord = attempt[attemptIndex];
|
|
2055
|
-
words.push({
|
|
2056
|
-
itemId: `w${ordinal}`,
|
|
2057
|
-
reference: referenceWord,
|
|
2058
|
-
attempt: attemptWord,
|
|
2059
|
-
similarity: similarity(referenceWord, attemptWord),
|
|
2060
|
-
status: referenceWord === attemptWord ? "correct" : "incorrect"
|
|
2061
|
-
});
|
|
2062
|
-
}
|
|
2063
|
-
return words;
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
2095
|
// src/scoring/text-match.ts
|
|
2067
2096
|
var COMBINING_MARKS_RE = /\p{M}/gu;
|
|
2068
2097
|
var PUNCTUATION_RE2 = /\p{P}/gu;
|
|
@@ -2187,6 +2216,7 @@ var DRAFT_ISSUE_SEVERITY = {
|
|
|
2187
2216
|
scoring_strategy_required: "incomplete",
|
|
2188
2217
|
pass_threshold_invalid: "invalid",
|
|
2189
2218
|
difficulty_level_invalid: "invalid",
|
|
2219
|
+
ai_permissions_invalid: "invalid",
|
|
2190
2220
|
feedback_empty: "incomplete",
|
|
2191
2221
|
redacted_data: "invalid",
|
|
2192
2222
|
media_type_required: "incomplete",
|
|
@@ -2421,6 +2451,30 @@ function checkSharedOptional(draft) {
|
|
|
2421
2451
|
)
|
|
2422
2452
|
);
|
|
2423
2453
|
}
|
|
2454
|
+
const ai = draft.ai;
|
|
2455
|
+
if (!isUnset(ai)) {
|
|
2456
|
+
if (!isRecord(ai)) {
|
|
2457
|
+
issues.push(
|
|
2458
|
+
issue(
|
|
2459
|
+
"ai_permissions_invalid",
|
|
2460
|
+
["ai"],
|
|
2461
|
+
"AI permissions must be an object of on/off flags, such as { hints: false }."
|
|
2462
|
+
)
|
|
2463
|
+
);
|
|
2464
|
+
} else {
|
|
2465
|
+
for (const key of ["explanations", "hints"]) {
|
|
2466
|
+
if (!isUnset(ai[key]) && typeof ai[key] !== "boolean") {
|
|
2467
|
+
issues.push(
|
|
2468
|
+
issue(
|
|
2469
|
+
"ai_permissions_invalid",
|
|
2470
|
+
["ai", key],
|
|
2471
|
+
"Set this to true or false, or remove it."
|
|
2472
|
+
)
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2424
2478
|
const feedback = draft.feedback;
|
|
2425
2479
|
if (isRecord(feedback)) {
|
|
2426
2480
|
for (const key of ["correct", "incorrect"]) {
|
|
@@ -2656,10 +2710,8 @@ function checkRecording(media) {
|
|
|
2656
2710
|
)
|
|
2657
2711
|
);
|
|
2658
2712
|
}
|
|
2659
|
-
|
|
2660
|
-
issues.push(
|
|
2661
|
-
issue("dc_captions_not_allowed", ["media", "captionsUrl"], CAPTIONS_ARE_THE_ANSWER)
|
|
2662
|
-
);
|
|
2713
|
+
for (const captions of captionFieldsOf(media, (captionsUrl) => !isUnset(captionsUrl))) {
|
|
2714
|
+
issues.push(issue("dc_captions_not_allowed", ["media", captions], CAPTIONS_ARE_THE_ANSWER));
|
|
2663
2715
|
}
|
|
2664
2716
|
return issues;
|
|
2665
2717
|
}
|
|
@@ -2684,10 +2736,8 @@ function checkSlowRecording(draft) {
|
|
|
2684
2736
|
DictationSlowMediaSchema
|
|
2685
2737
|
)
|
|
2686
2738
|
);
|
|
2687
|
-
|
|
2688
|
-
issues.push(
|
|
2689
|
-
issue("dc_captions_not_allowed", ["slowMedia", "captionsUrl"], CAPTIONS_ARE_THE_ANSWER)
|
|
2690
|
-
);
|
|
2739
|
+
for (const captions of captionFieldsOf(slow, (captionsUrl) => !isUnset(captionsUrl))) {
|
|
2740
|
+
issues.push(issue("dc_captions_not_allowed", ["slowMedia", captions], CAPTIONS_ARE_THE_ANSWER));
|
|
2691
2741
|
}
|
|
2692
2742
|
if (slow.playback !== void 0) {
|
|
2693
2743
|
issues.push(
|
|
@@ -4064,7 +4114,11 @@ var SHARED_PUBLIC_FIELDS = {
|
|
|
4064
4114
|
passThreshold: "public",
|
|
4065
4115
|
locale: "public",
|
|
4066
4116
|
learningObjectives: "public",
|
|
4067
|
-
difficultyLevel: "public"
|
|
4117
|
+
difficultyLevel: "public",
|
|
4118
|
+
// What an author allows AI to do for the item. Public, field by field: a
|
|
4119
|
+
// review that explains an answer must still honour an author who said no,
|
|
4120
|
+
// and an unclassified key parked inside it must not ride along.
|
|
4121
|
+
ai: { explanations: "public", hints: "public" }
|
|
4068
4122
|
};
|
|
4069
4123
|
var FEEDBACK_FIELD_POLICY = {
|
|
4070
4124
|
correct: "answer-key",
|
|
@@ -4426,5 +4480,7 @@ registerActivityType(readAloudType);
|
|
|
4426
4480
|
|
|
4427
4481
|
|
|
4428
4482
|
|
|
4429
|
-
|
|
4430
|
-
|
|
4483
|
+
|
|
4484
|
+
|
|
4485
|
+
exports.AiPermissionsSchema = AiPermissionsSchema; exports.FeedbackSchema = FeedbackSchema; exports.MediaUrlSchema = MediaUrlSchema; exports.NativeControlHintSchema = NativeControlHintSchema; exports.MediaPlaybackSchema = MediaPlaybackSchema; exports.MediaTrackSchema = MediaTrackSchema; exports.MediaSchema = MediaSchema; exports.RedactedMediaSchema = RedactedMediaSchema; exports.PLACEHOLDER_RE = PLACEHOLDER_RE; exports.TextMatchPolicySchema = TextMatchPolicySchema; exports.BlankConfigSchema = BlankConfigSchema; exports.FillInTheBlanksDataSchema = FillInTheBlanksDataSchema; exports.withValidationScope = withValidationScope; exports.DICTATION_MAX_TRANSCRIPT_LENGTH = DICTATION_MAX_TRANSCRIPT_LENGTH; exports.DICTATION_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH; exports.DICTATION_MAX_EQUIVALENCE_LENGTH = DICTATION_MAX_EQUIVALENCE_LENGTH; exports.DICTATION_MAX_EQUIVALENCES = DICTATION_MAX_EQUIVALENCES; exports.DICTATION_MAX_ACCEPTED_TRANSCRIPTS = DICTATION_MAX_ACCEPTED_TRANSCRIPTS; exports.codePointLength = codePointLength; exports.normalizeDictationText = normalizeDictationText; exports.dictationReferenceWords = dictationReferenceWords; exports.alignDictation = alignDictation; exports.STEP = STEP; exports.alignSequences = alignSequences; exports.ActivitySchemaError = ActivitySchemaError; exports.UnknownActivityTypeError = UnknownActivityTypeError; exports.RedactedScoringError = RedactedScoringError; exports.DeferredScoringError = DeferredScoringError; exports.DictationSlowMediaSchema = DictationSlowMediaSchema; exports.DictationEquivalenceSchema = DictationEquivalenceSchema; exports.DictationToleranceSchema = DictationToleranceSchema; exports.DictationDataSchema = DictationDataSchema; exports.groupCaptionsField = groupCaptionsField; exports.GROUP_CAPTIONS_REVEAL_DICTATION = GROUP_CAPTIONS_REVEAL_DICTATION; exports.DRAFT_ISSUE_SEVERITY = DRAFT_ISSUE_SEVERITY; exports.issue = issue; exports.isRecord = isRecord; exports.refusesEmpty = refusesEmpty; exports.isUnset = isUnset; exports.isUnwritten = isUnwritten; exports.isMissingId = isMissingId; exports.checkMedia = checkMedia; exports.covers = covers; exports.pathKey = pathKey; exports.coveredPathsOf = coveredPathsOf; exports.MultipleChoiceOptionMediaSchema = MultipleChoiceOptionMediaSchema; exports.MultipleChoiceOptionSchema = MultipleChoiceOptionSchema; exports.MultipleChoiceDataSchema = MultipleChoiceDataSchema; exports.READ_ALOUD_MAX_REFERENCE_LENGTH = READ_ALOUD_MAX_REFERENCE_LENGTH; exports.READ_ALOUD_MAX_SECONDS = READ_ALOUD_MAX_SECONDS; exports.READ_ALOUD_MAX_TAKES = READ_ALOUD_MAX_TAKES; exports.READ_ALOUD_MAX_DIMENSION_WEIGHT = READ_ALOUD_MAX_DIMENSION_WEIGHT; exports.SPEECH_ASSESSMENT_MAX_WORDS = SPEECH_ASSESSMENT_MAX_WORDS; exports.SPEECH_ASSESSMENT_MAX_TEXT_LENGTH = SPEECH_ASSESSMENT_MAX_TEXT_LENGTH; exports.CANONICAL_LOCALE_RE = CANONICAL_LOCALE_RE; exports.ReadAloudSlowMediaSchema = ReadAloudSlowMediaSchema; exports.ReadAloudDataSchema = ReadAloudDataSchema; exports.countWords = countWords; exports.GapSelectChoiceSchema = GapSelectChoiceSchema; exports.GapSelectBankSchema = GapSelectBankSchema; exports.GapSelectGapSchema = GapSelectGapSchema; exports.GapSelectDataSchema = GapSelectDataSchema; exports.WrittenResponseRubricCriterionSchema = WrittenResponseRubricCriterionSchema; exports.WrittenResponseRubricSchema = WrittenResponseRubricSchema; exports.WrittenResponseDataSchema = WrittenResponseDataSchema; exports.RedactedMultipleChoiceOptionMediaSchema = RedactedMultipleChoiceOptionMediaSchema; exports.RedactedMultipleChoiceOptionSchema = RedactedMultipleChoiceOptionSchema; exports.RedactedMultipleChoiceDataSchema = RedactedMultipleChoiceDataSchema; exports.RedactedBlankConfigSchema = RedactedBlankConfigSchema; exports.RedactedFillInTheBlanksDataSchema = RedactedFillInTheBlanksDataSchema; exports.RedactedGapSelectChoiceSchema = RedactedGapSelectChoiceSchema; exports.RedactedGapSelectBankSchema = RedactedGapSelectBankSchema; exports.RedactedGapSelectGapSchema = RedactedGapSelectGapSchema; exports.RedactedGapSelectDataSchema = RedactedGapSelectDataSchema; exports.RedactedWrittenResponseDataSchema = RedactedWrittenResponseDataSchema; exports.RedactedDictationSlowMediaSchema = RedactedDictationSlowMediaSchema; exports.RedactedDictationDataSchema = RedactedDictationDataSchema; exports.RedactedReadAloudDataSchema = RedactedReadAloudDataSchema; exports.levenshteinDistance = levenshteinDistance; exports.matchText = matchText; exports.defineActivityType = defineActivityType; exports.registerActivityType = registerActivityType; exports.getActivityTypeDescriptor = getActivityTypeDescriptor; exports.registeredActivityTypes = registeredActivityTypes; exports.MEDIA_FIELD_POLICY = MEDIA_FIELD_POLICY; exports.multipleChoiceType = multipleChoiceType; exports.fillInTheBlanksType = fillInTheBlanksType; exports.writtenResponseType = writtenResponseType; exports.gapSelectType = gapSelectType; exports.dictationType = dictationType; exports.readAloudType = readAloudType;
|
|
4486
|
+
//# sourceMappingURL=chunk-D4TOPSHV.cjs.map
|