@intellectif/lk-core 0.11.0 → 0.13.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 +38 -0
- package/README.md +2 -1
- package/dist/{activity-B2QxK15j.d.cts → activity-DfAmJ1sl.d.cts} +134 -7
- package/dist/{activity-B2QxK15j.d.ts → activity-DfAmJ1sl.d.ts} +134 -7
- package/dist/{chunk-3FEAEO3D.cjs → chunk-DQKJUMUW.cjs} +38 -15
- package/dist/chunk-DQKJUMUW.cjs.map +1 -0
- package/dist/{chunk-ZTY4UIUD.js → chunk-FGN4LLSP.js} +50 -6
- package/dist/chunk-FGN4LLSP.js.map +1 -0
- package/dist/{chunk-VTTRXGDD.cjs → chunk-FT7SAC3X.cjs} +4 -4
- package/dist/{chunk-VTTRXGDD.cjs.map → chunk-FT7SAC3X.cjs.map} +1 -1
- package/dist/{chunk-IXXMYJI7.js → chunk-PYP3HCRQ.js} +2 -2
- package/dist/{chunk-VBM5H6P5.cjs → chunk-UHQVIC3P.cjs} +58 -14
- package/dist/chunk-UHQVIC3P.cjs.map +1 -0
- package/dist/{chunk-FIS5KBCE.js → chunk-XKKKWKRH.js} +1973 -179
- package/dist/chunk-XKKKWKRH.js.map +1 -0
- package/dist/{chunk-7ODQRNR4.cjs → chunk-XONKWL6D.cjs} +1812 -18
- package/dist/chunk-XONKWL6D.cjs.map +1 -0
- package/dist/{chunk-XGCTAQSS.js → chunk-Y36S3X3L.js} +26 -3
- package/dist/chunk-Y36S3X3L.js.map +1 -0
- package/dist/{index-CRKtWP5G.d.ts → index-Bz9wUigA.d.cts} +181 -4
- package/dist/{index-B_scInLM.d.cts → index-CJbbVE9o.d.ts} +181 -4
- package/dist/index.cjs +349 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -6
- package/dist/index.d.ts +48 -6
- package/dist/index.js +322 -11
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +17 -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 +16 -2
- package/dist/scoring.cjs +19 -3
- package/dist/scoring.cjs.map +1 -1
- package/dist/scoring.d.cts +150 -5
- package/dist/scoring.d.ts +150 -5
- package/dist/scoring.js +18 -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/vectors/README.md +9 -1
- package/vectors/scoring.json +12023 -1
- package/dist/chunk-3FEAEO3D.cjs.map +0 -1
- package/dist/chunk-7ODQRNR4.cjs.map +0 -1
- package/dist/chunk-FIS5KBCE.js.map +0 -1
- package/dist/chunk-VBM5H6P5.cjs.map +0 -1
- package/dist/chunk-XGCTAQSS.js.map +0 -1
- package/dist/chunk-ZTY4UIUD.js.map +0 -1
- /package/dist/{chunk-IXXMYJI7.js.map → chunk-PYP3HCRQ.js.map} +0 -0
|
@@ -38,6 +38,876 @@ var DeferredScoringError = class extends Error {
|
|
|
38
38
|
activityType;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
// src/validation-scope.ts
|
|
42
|
+
var active = null;
|
|
43
|
+
function withValidationScope(run) {
|
|
44
|
+
if (active !== null) {
|
|
45
|
+
return run();
|
|
46
|
+
}
|
|
47
|
+
active = /* @__PURE__ */ new Map();
|
|
48
|
+
try {
|
|
49
|
+
return run();
|
|
50
|
+
} finally {
|
|
51
|
+
active = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function scopeCache(key, create) {
|
|
55
|
+
if (active === null) {
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
let value = active.get(key);
|
|
59
|
+
if (value === void 0) {
|
|
60
|
+
value = create();
|
|
61
|
+
active.set(key, value);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/scoring/dictation/normalize.ts
|
|
67
|
+
var DICTATION_MAX_TRANSCRIPT_LENGTH = 2e3;
|
|
68
|
+
var DICTATION_MAX_TEXT_LENGTH = 8e3;
|
|
69
|
+
var DICTATION_MAX_EQUIVALENCE_LENGTH = 200;
|
|
70
|
+
var DICTATION_MAX_EQUIVALENCES = 100;
|
|
71
|
+
var DICTATION_MAX_ACCEPTED_TRANSCRIPTS = 10;
|
|
72
|
+
var WORKING_LENGTH = 2 * DICTATION_MAX_TEXT_LENGTH;
|
|
73
|
+
var UNSPACED_REVEAL_MIN_CHARACTERS = 4;
|
|
74
|
+
var cp = (...points) => String.fromCodePoint(...points);
|
|
75
|
+
var NEXT_LINE = cp(133);
|
|
76
|
+
var MIDDLE_DOT = cp(183);
|
|
77
|
+
var TIBETAN_TSHEG = cp(3851);
|
|
78
|
+
var COMBINING_KEYCAP = cp(8419);
|
|
79
|
+
var WAVING_BLACK_FLAG = 127988;
|
|
80
|
+
var ZWNJ = 8204;
|
|
81
|
+
var ZWJ = 8205;
|
|
82
|
+
var MONGOLIAN_VOWEL_SEPARATOR = 6158;
|
|
83
|
+
var JOINERS = cp(ZWNJ, ZWJ, MONGOLIAN_VOWEL_SEPARATOR);
|
|
84
|
+
var MONGOLIAN_SELECTORS = cp(6155, 6156, 6157, 6159);
|
|
85
|
+
var TAGS = `${cp(917536)}-${cp(917631)}`;
|
|
86
|
+
var STRETCHERS = cp(1600, 2042);
|
|
87
|
+
var COMPATIBILITY_RE = new RegExp(
|
|
88
|
+
`[${[
|
|
89
|
+
cp(170),
|
|
90
|
+
cp(178, 179, 181, 185, 186),
|
|
91
|
+
`${cp(188)}-${cp(190)}`,
|
|
92
|
+
`${cp(306)}-${cp(307)}`,
|
|
93
|
+
`${cp(319)}-${cp(320)}`,
|
|
94
|
+
cp(329),
|
|
95
|
+
`${cp(452)}-${cp(460)}`,
|
|
96
|
+
`${cp(497)}-${cp(499)}`,
|
|
97
|
+
`${cp(8304)}-${cp(8351)}`,
|
|
98
|
+
`${cp(8448)}-${cp(8591)}`,
|
|
99
|
+
`${cp(9312)}-${cp(9471)}`,
|
|
100
|
+
`${cp(11904)}-${cp(12255)}`,
|
|
101
|
+
`${cp(12881)}-${cp(12895)}`,
|
|
102
|
+
`${cp(12977)}-${cp(12991)}`,
|
|
103
|
+
`${cp(64256)}-${cp(65023)}`,
|
|
104
|
+
`${cp(65040)}-${cp(65049)}`,
|
|
105
|
+
`${cp(65072)}-${cp(65135)}`,
|
|
106
|
+
`${cp(65136)}-${cp(65278)}`,
|
|
107
|
+
`${cp(65281)}-${cp(65373)}`,
|
|
108
|
+
`${cp(65375)}-${cp(65506)}`,
|
|
109
|
+
`${cp(65508)}-${cp(65518)}`
|
|
110
|
+
].join("")}]`,
|
|
111
|
+
"gu"
|
|
112
|
+
);
|
|
113
|
+
var COMPATIBILITY_JAMO = new Map(
|
|
114
|
+
Array.from({ length: 12686 - 12593 + 1 }, (_, index) => {
|
|
115
|
+
const jamo = cp(12593 + index);
|
|
116
|
+
return [jamo.normalize("NFKC"), jamo];
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
var SEQUENCE_FOLDS = /* @__PURE__ */ new Map([
|
|
120
|
+
[cp(3363, 3405, ZWJ), cp(3450)],
|
|
121
|
+
[cp(3368, 3405, ZWJ), cp(3451)],
|
|
122
|
+
[cp(3376, 3405, ZWJ), cp(3452)],
|
|
123
|
+
[cp(3378, 3405, ZWJ), cp(3453)],
|
|
124
|
+
[cp(3379, 3405, ZWJ), cp(3454)],
|
|
125
|
+
[cp(3349, 3405, ZWJ), cp(3455)],
|
|
126
|
+
[cp(3374, 3405, ZWJ), cp(3412)],
|
|
127
|
+
[cp(3375, 3405, ZWJ), cp(3413)],
|
|
128
|
+
[cp(3380, 3405, ZWJ), cp(3414)],
|
|
129
|
+
[cp(2468, 2509, ZWJ), cp(2510)],
|
|
130
|
+
[cp(3368, 3405, 3377), cp(3451, 3405, 3377)],
|
|
131
|
+
[cp(2352, 2381, ZWJ), cp(2353, 2381)],
|
|
132
|
+
[cp(2353, 2381, ZWJ), cp(2353, 2381)]
|
|
133
|
+
]);
|
|
134
|
+
var SEQUENCE_FOLDS_RE = new RegExp([...SEQUENCE_FOLDS.keys()].join("|"), "gu");
|
|
135
|
+
var THAI_SARA_AM_PARTS_RE = new RegExp(
|
|
136
|
+
`${cp(3661)}([${cp(3656)}-${cp(3659)}]*)${cp(3634)}`,
|
|
137
|
+
"gu"
|
|
138
|
+
);
|
|
139
|
+
var LAO_SARA_AM_PARTS_RE = new RegExp(
|
|
140
|
+
`${cp(3789)}([${cp(3784)}-${cp(3787)}]*)${cp(3762)}`,
|
|
141
|
+
"gu"
|
|
142
|
+
);
|
|
143
|
+
function foldSequences(text) {
|
|
144
|
+
return text.replace(SEQUENCE_FOLDS_RE, (sequence) => SEQUENCE_FOLDS.get(sequence)).replace(THAI_SARA_AM_PARTS_RE, `$1${cp(3635)}`).replace(LAO_SARA_AM_PARTS_RE, `$1${cp(3763)}`);
|
|
145
|
+
}
|
|
146
|
+
var FOLDED = new Map(
|
|
147
|
+
[
|
|
148
|
+
[8216, "'"],
|
|
149
|
+
// LEFT SINGLE QUOTATION MARK
|
|
150
|
+
[8217, "'"],
|
|
151
|
+
// RIGHT SINGLE QUOTATION MARK (the smart apostrophe)
|
|
152
|
+
[8218, "'"],
|
|
153
|
+
// SINGLE LOW-9 QUOTATION MARK
|
|
154
|
+
[8219, "'"],
|
|
155
|
+
// SINGLE HIGH-REVERSED-9 QUOTATION MARK
|
|
156
|
+
[8242, "'"],
|
|
157
|
+
// PRIME
|
|
158
|
+
[700, "'"],
|
|
159
|
+
// MODIFIER LETTER APOSTROPHE
|
|
160
|
+
[180, "'"],
|
|
161
|
+
// ACUTE ACCENT, used as an apostrophe
|
|
162
|
+
[96, "'"],
|
|
163
|
+
// GRAVE ACCENT, used as an apostrophe
|
|
164
|
+
[1523, "'"],
|
|
165
|
+
// HEBREW PUNCTUATION GERESH
|
|
166
|
+
[8220, '"'],
|
|
167
|
+
// LEFT DOUBLE QUOTATION MARK
|
|
168
|
+
[8221, '"'],
|
|
169
|
+
// RIGHT DOUBLE QUOTATION MARK
|
|
170
|
+
[8222, '"'],
|
|
171
|
+
// DOUBLE LOW-9 QUOTATION MARK
|
|
172
|
+
[8243, '"'],
|
|
173
|
+
// DOUBLE PRIME
|
|
174
|
+
[1524, '"'],
|
|
175
|
+
// HEBREW PUNCTUATION GERSHAYIM
|
|
176
|
+
[8208, "-"],
|
|
177
|
+
// HYPHEN
|
|
178
|
+
[8209, "-"],
|
|
179
|
+
// NON-BREAKING HYPHEN
|
|
180
|
+
[1470, "-"],
|
|
181
|
+
// HEBREW PUNCTUATION MAQAF
|
|
182
|
+
[1418, "-"],
|
|
183
|
+
// ARMENIAN HYPHEN
|
|
184
|
+
[3852, TIBETAN_TSHEG],
|
|
185
|
+
// TIBETAN MARK DELIMITER TSHEG BSTAR
|
|
186
|
+
[65374, cp(12316)]
|
|
187
|
+
// FULLWIDTH TILDE, which a Japanese keyboard types for the WAVE DASH
|
|
188
|
+
].map(([point, folded]) => [cp(point), folded])
|
|
189
|
+
);
|
|
190
|
+
var FOLDED_RE = new RegExp(`[${[...FOLDED.keys()].join("")}]`, "gu");
|
|
191
|
+
var DRAWN_FORMAT_CHARACTERS = `${cp(1536)}-${cp(1541)}${cp(1757, 1807, 2192, 2193, 2274, 69821, 69837)}`;
|
|
192
|
+
var INVISIBLE_RE = new RegExp(
|
|
193
|
+
`(?![\\t\\n\\v\\f\\r${NEXT_LINE}${JOINERS}${MONGOLIAN_SELECTORS}${TAGS}${DRAWN_FORMAT_CHARACTERS}])[\\p{Cc}\\p{Cf}\\p{Default_Ignorable_Code_Point}${STRETCHERS}]`,
|
|
194
|
+
"gu"
|
|
195
|
+
);
|
|
196
|
+
var REPEATED_JOINER_RE = new RegExp(`([${JOINERS}])\\1+`, "gu");
|
|
197
|
+
var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
198
|
+
var REPLACEMENT_CHARACTER = cp(65533);
|
|
199
|
+
var CONTEXTUAL_RE = new RegExp(`[${JOINERS}${MONGOLIAN_SELECTORS}${TAGS}]`, "u");
|
|
200
|
+
var WHITESPACE_RE = new RegExp(`[\\s${NEXT_LINE}]+`, "gu");
|
|
201
|
+
var UNSPACED_CLASS = "\\p{Script_Extensions=Han}\\p{Script_Extensions=Hiragana}\\p{Script_Extensions=Katakana}\\p{Script_Extensions=Thai}\\p{Script_Extensions=Lao}\\p{Script_Extensions=Khmer}\\p{Script_Extensions=Myanmar}";
|
|
202
|
+
var UNSPACED_CHARACTER = `(?!\\p{Nd})[${UNSPACED_CLASS}]`;
|
|
203
|
+
var IN_WORD_PUNCTUATION = `'\\-${MIDDLE_DOT}${TIBETAN_TSHEG}`;
|
|
204
|
+
var PUNCTUATION_RE = new RegExp(
|
|
205
|
+
[
|
|
206
|
+
`[${IN_WORD_PUNCTUATION}](?<![\\p{L}\\p{M}\\p{N}][${IN_WORD_PUNCTUATION}])`,
|
|
207
|
+
`[${IN_WORD_PUNCTUATION}](?![\\p{L}\\p{M}\\p{N}])`,
|
|
208
|
+
`${MIDDLE_DOT}(?<=${UNSPACED_CHARACTER}${MIDDLE_DOT})`,
|
|
209
|
+
`${MIDDLE_DOT}(?=${UNSPACED_CHARACTER})`,
|
|
210
|
+
`(?![#*]${COMBINING_KEYCAP})[^\\P{P}${IN_WORD_PUNCTUATION}]`
|
|
211
|
+
].join("|"),
|
|
212
|
+
"gu"
|
|
213
|
+
);
|
|
214
|
+
var ALL_PUNCTUATION_RE = new RegExp(`(?![#*]${COMBINING_KEYCAP})\\p{P}`, "gu");
|
|
215
|
+
var DIGIT_SEPARATOR_RE = new RegExp(`(?<=\\p{N})[.,'${cp(1643, 1644)}](?=\\p{N})`, "gu");
|
|
216
|
+
var UNSPACED_LETTERS_RE = new RegExp(`(?![\\p{M}\\p{Nd}])[${UNSPACED_CLASS}]`, "gu");
|
|
217
|
+
var SPACE_BESIDE_UNSPACED_RE = new RegExp(
|
|
218
|
+
`(?<=${UNSPACED_CHARACTER}) (?=[\\p{L}\\p{M}\\p{N}])|(?<=[\\p{L}\\p{M}\\p{N}]) (?=${UNSPACED_CHARACTER})`,
|
|
219
|
+
"gu"
|
|
220
|
+
);
|
|
221
|
+
var WORD_RE = new RegExp(`^[\\p{L}\\p{M}\\p{N}${JOINERS}]$`, "u");
|
|
222
|
+
var UNSPACED_CHARACTER_RE = new RegExp(`^${UNSPACED_CHARACTER}$`, "u");
|
|
223
|
+
var EXTENDING_RE = new RegExp(`^[\\p{M}${JOINERS}${TAGS}]$`, "u");
|
|
224
|
+
var WORD = 1;
|
|
225
|
+
var UNSPACED = 2;
|
|
226
|
+
var EXTENDING = 4;
|
|
227
|
+
var KNOWN = 8;
|
|
228
|
+
var FLAGS = [];
|
|
229
|
+
function flagsOfCharacter(character) {
|
|
230
|
+
return KNOWN | (WORD_RE.test(character) ? WORD : 0) | (UNSPACED_CHARACTER_RE.test(character) ? UNSPACED : 0) | (EXTENDING_RE.test(character) ? EXTENDING : 0);
|
|
231
|
+
}
|
|
232
|
+
function flagsAt(text, index) {
|
|
233
|
+
const point = text.codePointAt(index);
|
|
234
|
+
let plane = FLAGS[point >> 16];
|
|
235
|
+
if (plane === void 0) {
|
|
236
|
+
plane = new Uint8Array(65536);
|
|
237
|
+
FLAGS[point >> 16] = plane;
|
|
238
|
+
}
|
|
239
|
+
const offset = point & 65535;
|
|
240
|
+
let flags = plane[offset];
|
|
241
|
+
if (flags === 0) {
|
|
242
|
+
flags = flagsOfCharacter(String.fromCodePoint(point));
|
|
243
|
+
plane[offset] = flags;
|
|
244
|
+
}
|
|
245
|
+
return flags;
|
|
246
|
+
}
|
|
247
|
+
var VIRAMAS = /* @__PURE__ */ new Set([
|
|
248
|
+
2381,
|
|
249
|
+
2509,
|
|
250
|
+
2637,
|
|
251
|
+
2765,
|
|
252
|
+
2893,
|
|
253
|
+
3021,
|
|
254
|
+
3149,
|
|
255
|
+
3277,
|
|
256
|
+
3387,
|
|
257
|
+
3388,
|
|
258
|
+
3405,
|
|
259
|
+
3530,
|
|
260
|
+
3642,
|
|
261
|
+
3770,
|
|
262
|
+
3972,
|
|
263
|
+
4153,
|
|
264
|
+
4154,
|
|
265
|
+
5908,
|
|
266
|
+
5909,
|
|
267
|
+
5940,
|
|
268
|
+
6098,
|
|
269
|
+
6752,
|
|
270
|
+
6980,
|
|
271
|
+
7082,
|
|
272
|
+
7083,
|
|
273
|
+
7154,
|
|
274
|
+
7155,
|
|
275
|
+
11647,
|
|
276
|
+
43014,
|
|
277
|
+
43052,
|
|
278
|
+
43204,
|
|
279
|
+
43347,
|
|
280
|
+
43456,
|
|
281
|
+
43766,
|
|
282
|
+
44013,
|
|
283
|
+
68159,
|
|
284
|
+
69702,
|
|
285
|
+
69744,
|
|
286
|
+
69759,
|
|
287
|
+
69817,
|
|
288
|
+
69939,
|
|
289
|
+
69940,
|
|
290
|
+
70080,
|
|
291
|
+
70197,
|
|
292
|
+
70378,
|
|
293
|
+
70477,
|
|
294
|
+
70606,
|
|
295
|
+
70607,
|
|
296
|
+
70608,
|
|
297
|
+
70722,
|
|
298
|
+
70850,
|
|
299
|
+
71103,
|
|
300
|
+
71231,
|
|
301
|
+
71350,
|
|
302
|
+
71467,
|
|
303
|
+
71737,
|
|
304
|
+
71997,
|
|
305
|
+
71998,
|
|
306
|
+
72160,
|
|
307
|
+
72244,
|
|
308
|
+
72263,
|
|
309
|
+
72345,
|
|
310
|
+
72767,
|
|
311
|
+
73028,
|
|
312
|
+
73029,
|
|
313
|
+
73111,
|
|
314
|
+
73537,
|
|
315
|
+
73538,
|
|
316
|
+
90415
|
|
317
|
+
]);
|
|
318
|
+
var JOINS_FORWARD = [
|
|
319
|
+
[1568, 1568],
|
|
320
|
+
[1574, 1574],
|
|
321
|
+
[1576, 1576],
|
|
322
|
+
[1578, 1582],
|
|
323
|
+
[1587, 1599],
|
|
324
|
+
[1601, 1607],
|
|
325
|
+
[1609, 1610],
|
|
326
|
+
[1646, 1647],
|
|
327
|
+
[1656, 1671],
|
|
328
|
+
[1690, 1727],
|
|
329
|
+
[1729, 1730],
|
|
330
|
+
[1740, 1740],
|
|
331
|
+
[1742, 1742],
|
|
332
|
+
[1744, 1745],
|
|
333
|
+
[1786, 1788],
|
|
334
|
+
[1791, 1791],
|
|
335
|
+
[1810, 1812],
|
|
336
|
+
[1818, 1821],
|
|
337
|
+
[1823, 1831],
|
|
338
|
+
[1833, 1833],
|
|
339
|
+
[1835, 1835],
|
|
340
|
+
[1837, 1838],
|
|
341
|
+
[1870, 1880],
|
|
342
|
+
[1884, 1898],
|
|
343
|
+
[1901, 1904],
|
|
344
|
+
[1906, 1906],
|
|
345
|
+
[1909, 1911],
|
|
346
|
+
[1914, 1919],
|
|
347
|
+
[1994, 2026],
|
|
348
|
+
[2113, 2117],
|
|
349
|
+
[2120, 2120],
|
|
350
|
+
[2122, 2131],
|
|
351
|
+
[2133, 2133],
|
|
352
|
+
[2144, 2144],
|
|
353
|
+
[2146, 2149],
|
|
354
|
+
[2152, 2152],
|
|
355
|
+
[2182, 2182],
|
|
356
|
+
[2185, 2189],
|
|
357
|
+
[2208, 2217],
|
|
358
|
+
[2223, 2224],
|
|
359
|
+
[2227, 2232],
|
|
360
|
+
[2234, 2248],
|
|
361
|
+
[6151, 6151],
|
|
362
|
+
[6176, 6264],
|
|
363
|
+
[6279, 6312],
|
|
364
|
+
[6314, 6314],
|
|
365
|
+
[43072, 43122],
|
|
366
|
+
[68288, 68292],
|
|
367
|
+
[68301, 68301],
|
|
368
|
+
[68307, 68316],
|
|
369
|
+
[68318, 68320],
|
|
370
|
+
[68331, 68334],
|
|
371
|
+
[68480, 68480],
|
|
372
|
+
[68482, 68482],
|
|
373
|
+
[68486, 68488],
|
|
374
|
+
[68490, 68491],
|
|
375
|
+
[68493, 68493],
|
|
376
|
+
[68496, 68496],
|
|
377
|
+
[68525, 68526],
|
|
378
|
+
[68864, 68897],
|
|
379
|
+
[68899, 68899],
|
|
380
|
+
[69315, 69316],
|
|
381
|
+
[69424, 69426],
|
|
382
|
+
[69428, 69444],
|
|
383
|
+
[69457, 69459],
|
|
384
|
+
[69488, 69491],
|
|
385
|
+
[69494, 69505],
|
|
386
|
+
[69552, 69552],
|
|
387
|
+
[69554, 69555],
|
|
388
|
+
[69560, 69560],
|
|
389
|
+
[69563, 69564],
|
|
390
|
+
[69566, 69567],
|
|
391
|
+
[69569, 69569],
|
|
392
|
+
[69572, 69572],
|
|
393
|
+
[69578, 69579],
|
|
394
|
+
[125184, 125251]
|
|
395
|
+
];
|
|
396
|
+
var JOINS_BACKWARD = [
|
|
397
|
+
[1568, 1568],
|
|
398
|
+
[1570, 1599],
|
|
399
|
+
[1601, 1610],
|
|
400
|
+
[1646, 1647],
|
|
401
|
+
[1649, 1651],
|
|
402
|
+
[1653, 1747],
|
|
403
|
+
[1749, 1749],
|
|
404
|
+
[1774, 1775],
|
|
405
|
+
[1786, 1788],
|
|
406
|
+
[1791, 1791],
|
|
407
|
+
[1808, 1808],
|
|
408
|
+
[1810, 1839],
|
|
409
|
+
[1869, 1919],
|
|
410
|
+
[1994, 2026],
|
|
411
|
+
[2112, 2136],
|
|
412
|
+
[2144, 2144],
|
|
413
|
+
[2146, 2149],
|
|
414
|
+
[2151, 2154],
|
|
415
|
+
[2160, 2178],
|
|
416
|
+
[2182, 2182],
|
|
417
|
+
[2185, 2190],
|
|
418
|
+
[2208, 2220],
|
|
419
|
+
[2222, 2248],
|
|
420
|
+
[6151, 6151],
|
|
421
|
+
[6176, 6264],
|
|
422
|
+
[6279, 6312],
|
|
423
|
+
[6314, 6314],
|
|
424
|
+
[43072, 43121],
|
|
425
|
+
[68288, 68293],
|
|
426
|
+
[68295, 68295],
|
|
427
|
+
[68297, 68298],
|
|
428
|
+
[68302, 68310],
|
|
429
|
+
[68312, 68321],
|
|
430
|
+
[68324, 68324],
|
|
431
|
+
[68331, 68335],
|
|
432
|
+
[68480, 68497],
|
|
433
|
+
[68521, 68526],
|
|
434
|
+
[68865, 68899],
|
|
435
|
+
[69314, 69316],
|
|
436
|
+
[69424, 69444],
|
|
437
|
+
[69457, 69460],
|
|
438
|
+
[69488, 69505],
|
|
439
|
+
[69552, 69552],
|
|
440
|
+
[69554, 69558],
|
|
441
|
+
[69560, 69567],
|
|
442
|
+
[69569, 69572],
|
|
443
|
+
[69577, 69578],
|
|
444
|
+
[125184, 125251]
|
|
445
|
+
];
|
|
446
|
+
var TRANSPARENT_RE = new RegExp(`^[\\p{Mn}\\p{Me}${cp(125259)}${TAGS}]$`, "u");
|
|
447
|
+
function collapseWhitespace(text) {
|
|
448
|
+
return text.replace(WHITESPACE_RE, " ").trim();
|
|
449
|
+
}
|
|
450
|
+
function codePointLength(text) {
|
|
451
|
+
let length = 0;
|
|
452
|
+
for (const _ of text) {
|
|
453
|
+
length += 1;
|
|
454
|
+
}
|
|
455
|
+
return length;
|
|
456
|
+
}
|
|
457
|
+
function truncateCodePoints(text, max) {
|
|
458
|
+
if (text.length <= max) {
|
|
459
|
+
return text;
|
|
460
|
+
}
|
|
461
|
+
let points = 0;
|
|
462
|
+
let units = 0;
|
|
463
|
+
for (const character of text) {
|
|
464
|
+
if (points === max) {
|
|
465
|
+
return text.slice(0, units);
|
|
466
|
+
}
|
|
467
|
+
points += 1;
|
|
468
|
+
units += character.length;
|
|
469
|
+
}
|
|
470
|
+
return text;
|
|
471
|
+
}
|
|
472
|
+
function isLongerThan(text, max) {
|
|
473
|
+
if (text.length <= max) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
return text.length > 2 * max || truncateCodePoints(text, max).length < text.length;
|
|
477
|
+
}
|
|
478
|
+
function inRanges(point, ranges) {
|
|
479
|
+
return ranges.some(([first, last]) => point >= first && point <= last);
|
|
480
|
+
}
|
|
481
|
+
function isMongolianLetterOrMark(point) {
|
|
482
|
+
return point !== void 0 && point >= 6176 && point <= 6314;
|
|
483
|
+
}
|
|
484
|
+
function isMongolianSelector(point) {
|
|
485
|
+
return point !== void 0 && (point >= 6155 && point <= 6157 || point === 6159);
|
|
486
|
+
}
|
|
487
|
+
function letterBefore(points, index) {
|
|
488
|
+
for (let at = index - 1; at >= 0; at -= 1) {
|
|
489
|
+
const point = points[at];
|
|
490
|
+
if (!TRANSPARENT_RE.test(point)) {
|
|
491
|
+
return point.codePointAt(0);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return void 0;
|
|
495
|
+
}
|
|
496
|
+
function letterAfter(points, index) {
|
|
497
|
+
for (let at = index + 1; at < points.length; at += 1) {
|
|
498
|
+
const point = points[at];
|
|
499
|
+
if (!TRANSPARENT_RE.test(point)) {
|
|
500
|
+
return point.codePointAt(0);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return void 0;
|
|
504
|
+
}
|
|
505
|
+
var LETTER_OR_MARK_RE = /^[\p{L}\p{M}]$/u;
|
|
506
|
+
function keepsInContext(points, index) {
|
|
507
|
+
const point = points[index].codePointAt(0);
|
|
508
|
+
const previous = points[index - 1];
|
|
509
|
+
const afterVirama = previous !== void 0 && VIRAMAS.has(previous.codePointAt(0));
|
|
510
|
+
if (point === ZWJ) {
|
|
511
|
+
const next = points[index + 1];
|
|
512
|
+
return afterVirama || previous !== void 0 && LETTER_OR_MARK_RE.test(previous) && next !== void 0 && VIRAMAS.has(next.codePointAt(0));
|
|
513
|
+
}
|
|
514
|
+
if (point === ZWNJ) {
|
|
515
|
+
if (afterVirama) {
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
const before = letterBefore(points, index);
|
|
519
|
+
const after = letterAfter(points, index);
|
|
520
|
+
return before !== void 0 && after !== void 0 && inRanges(before, JOINS_FORWARD) && inRanges(after, JOINS_BACKWARD);
|
|
521
|
+
}
|
|
522
|
+
const previousPoint = previous?.codePointAt(0);
|
|
523
|
+
if (point === MONGOLIAN_VOWEL_SEPARATOR) {
|
|
524
|
+
return (isMongolianLetterOrMark(previousPoint) || isMongolianSelector(previousPoint)) && isMongolianLetterOrMark(points[index + 1]?.codePointAt(0));
|
|
525
|
+
}
|
|
526
|
+
return isMongolianLetterOrMark(previousPoint);
|
|
527
|
+
}
|
|
528
|
+
function settle(text) {
|
|
529
|
+
let current = text;
|
|
530
|
+
for (; ; ) {
|
|
531
|
+
let next = foldSequences(current);
|
|
532
|
+
if (CONTEXTUAL_RE.test(next)) {
|
|
533
|
+
const points = Array.from(next.replace(REPEATED_JOINER_RE, "$1"));
|
|
534
|
+
let inFlag = false;
|
|
535
|
+
next = points.filter((character, index) => {
|
|
536
|
+
const point = character.codePointAt(0);
|
|
537
|
+
if (point >= 917536 && point <= 917631) {
|
|
538
|
+
return inFlag;
|
|
539
|
+
}
|
|
540
|
+
inFlag = point === WAVING_BLACK_FLAG;
|
|
541
|
+
return !CONTEXTUAL_RE.test(character) || keepsInContext(points, index);
|
|
542
|
+
}).join("");
|
|
543
|
+
}
|
|
544
|
+
next = collapseWhitespace(next).normalize("NFC");
|
|
545
|
+
if (next === current) {
|
|
546
|
+
return current;
|
|
547
|
+
}
|
|
548
|
+
current = next;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
var CARRIED_MARKS_RE = new RegExp(`^[ ${cp(1600)}]?(\\p{M}+)$`, "u");
|
|
552
|
+
var SPACING_CHARACTER_RE = new RegExp(`[\\s${NEXT_LINE}]`, "u");
|
|
553
|
+
var PUNCTUATION_CHARACTER_RE = /^\p{P}$/u;
|
|
554
|
+
function foldCompatibility(character, offset, text) {
|
|
555
|
+
const folded = character.normalize("NFKC");
|
|
556
|
+
const carried = CARRIED_MARKS_RE.exec(folded);
|
|
557
|
+
if (carried === null) {
|
|
558
|
+
return COMPATIBILITY_JAMO.get(folded) ?? folded;
|
|
559
|
+
}
|
|
560
|
+
return PUNCTUATION_CHARACTER_RE.test(character) || offset === 0 || SPACING_CHARACTER_RE.test(text.charAt(offset - 1)) ? character : carried[1];
|
|
561
|
+
}
|
|
562
|
+
function preStripNormalize(text) {
|
|
563
|
+
return settle(
|
|
564
|
+
collapseWhitespace(
|
|
565
|
+
text.replace(LONE_SURROGATE_RE, REPLACEMENT_CHARACTER).normalize("NFC").replace(COMPATIBILITY_RE, foldCompatibility).replace(INVISIBLE_RE, "").replace(FOLDED_RE, (character) => FOLDED.get(character)).toLowerCase()
|
|
566
|
+
).normalize("NFC")
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
function settleWithout(text, pattern, replacement) {
|
|
570
|
+
let current = text;
|
|
571
|
+
for (; ; ) {
|
|
572
|
+
const next = settle(current.replace(pattern, replacement));
|
|
573
|
+
if (next === current) {
|
|
574
|
+
return current;
|
|
575
|
+
}
|
|
576
|
+
current = next;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function stripPunctuation(text) {
|
|
580
|
+
return settleWithout(text, PUNCTUATION_RE, "");
|
|
581
|
+
}
|
|
582
|
+
function spacePunctuation(text) {
|
|
583
|
+
return settleWithout(text.replace(DIGIT_SEPARATOR_RE, ""), ALL_PUNCTUATION_RE, " ");
|
|
584
|
+
}
|
|
585
|
+
function isHighSurrogate(unit) {
|
|
586
|
+
return unit >= 55296 && unit <= 56319;
|
|
587
|
+
}
|
|
588
|
+
function isLowSurrogate(unit) {
|
|
589
|
+
return unit >= 56320 && unit <= 57343;
|
|
590
|
+
}
|
|
591
|
+
function startOfCodePointBefore(text, index) {
|
|
592
|
+
return index >= 2 && isLowSurrogate(text.charCodeAt(index - 1)) && isHighSurrogate(text.charCodeAt(index - 2)) ? index - 2 : index - 1;
|
|
593
|
+
}
|
|
594
|
+
function lastCodePointAfter(previous, appended) {
|
|
595
|
+
return appended === "" ? previous : appended.slice(startOfCodePointBefore(appended, appended.length));
|
|
596
|
+
}
|
|
597
|
+
function isAsciiWordUnit(unit) {
|
|
598
|
+
return unit >= 48 && unit <= 57 || unit >= 97 && unit <= 122;
|
|
599
|
+
}
|
|
600
|
+
function isWordAt(text, index) {
|
|
601
|
+
const unit = text.charCodeAt(index);
|
|
602
|
+
return unit < 128 ? isAsciiWordUnit(unit) : (flagsAt(text, index) & WORD) !== 0;
|
|
603
|
+
}
|
|
604
|
+
function isSpacedWordAt(text, index) {
|
|
605
|
+
const unit = text.charCodeAt(index);
|
|
606
|
+
return unit < 128 ? isAsciiWordUnit(unit) : (flagsAt(text, index) & (WORD | UNSPACED)) === WORD;
|
|
607
|
+
}
|
|
608
|
+
function isExtendingAt(text, index) {
|
|
609
|
+
return text.charCodeAt(index) >= 128 && (flagsAt(text, index) & EXTENDING) !== 0;
|
|
610
|
+
}
|
|
611
|
+
function edgeOf(text, index, unspaced) {
|
|
612
|
+
if (!isWordAt(text, index)) {
|
|
613
|
+
return "none";
|
|
614
|
+
}
|
|
615
|
+
return isSpacedWordAt(text, index) ? "spaced" : unspaced;
|
|
616
|
+
}
|
|
617
|
+
function phraseOf(text, unspaced) {
|
|
618
|
+
return {
|
|
619
|
+
text,
|
|
620
|
+
before: edgeOf(text, 0, unspaced),
|
|
621
|
+
after: edgeOf(text, startOfCodePointBefore(text, text.length), unspaced)
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function blocks(edge, text, neighbour) {
|
|
625
|
+
if (edge === "none") {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
return edge === "spaced" ? isSpacedWordAt(text, neighbour) : isWordAt(text, neighbour);
|
|
629
|
+
}
|
|
630
|
+
function isMatchAt(text, phrase, at) {
|
|
631
|
+
const end = at + phrase.text.length;
|
|
632
|
+
if (end < text.length && (isExtendingAt(text, end) || blocks(phrase.after, text, end))) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
return !(at > 0 && blocks(phrase.before, text, startOfCodePointBefore(text, at)));
|
|
636
|
+
}
|
|
637
|
+
function indexOfPhrase(text, phrase, start) {
|
|
638
|
+
const needle = phrase.text;
|
|
639
|
+
let at = text.indexOf(needle, start);
|
|
640
|
+
while (at !== -1) {
|
|
641
|
+
if (phrase.before === "spaced" && at > 0 && isSpacedWordAt(text, startOfCodePointBefore(text, at))) {
|
|
642
|
+
let next = at;
|
|
643
|
+
while (next < text.length && isSpacedWordAt(text, next)) {
|
|
644
|
+
next += isHighSurrogate(text.charCodeAt(next)) && isLowSurrogate(text.charCodeAt(next + 1)) ? 2 : 1;
|
|
645
|
+
}
|
|
646
|
+
at = text.indexOf(needle, next);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (isMatchAt(text, phrase, at)) {
|
|
650
|
+
return at;
|
|
651
|
+
}
|
|
652
|
+
at = text.indexOf(needle, at + 1);
|
|
653
|
+
}
|
|
654
|
+
return -1;
|
|
655
|
+
}
|
|
656
|
+
function containsPhrase(text, phrase) {
|
|
657
|
+
const needle = phrase.text;
|
|
658
|
+
const length = needle.length;
|
|
659
|
+
if (length === 0 || length > text.length) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
const fallback = new Int32Array(length);
|
|
663
|
+
for (let index = 1, matched = 0; index < length; index += 1) {
|
|
664
|
+
while (matched > 0 && needle.charCodeAt(index) !== needle.charCodeAt(matched)) {
|
|
665
|
+
matched = fallback[matched - 1];
|
|
666
|
+
}
|
|
667
|
+
if (needle.charCodeAt(index) === needle.charCodeAt(matched)) {
|
|
668
|
+
matched += 1;
|
|
669
|
+
}
|
|
670
|
+
fallback[index] = matched;
|
|
671
|
+
}
|
|
672
|
+
for (let index = 0, matched = 0; index < text.length; index += 1) {
|
|
673
|
+
while (matched > 0 && text.charCodeAt(index) !== needle.charCodeAt(matched)) {
|
|
674
|
+
matched = fallback[matched - 1];
|
|
675
|
+
}
|
|
676
|
+
if (text.charCodeAt(index) === needle.charCodeAt(matched)) {
|
|
677
|
+
matched += 1;
|
|
678
|
+
}
|
|
679
|
+
if (matched === length) {
|
|
680
|
+
if (isMatchAt(text, phrase, index - length + 1)) {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
matched = fallback[matched - 1];
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
function compileRules(tolerance) {
|
|
689
|
+
const equivalences = tolerance?.equivalences;
|
|
690
|
+
if (!Array.isArray(equivalences)) {
|
|
691
|
+
return { rules: [], signature: "[]" };
|
|
692
|
+
}
|
|
693
|
+
const rules = [];
|
|
694
|
+
for (const rule of equivalences.slice(0, DICTATION_MAX_EQUIVALENCES)) {
|
|
695
|
+
const rawFrom = rule?.from;
|
|
696
|
+
const rawTo = rule?.to;
|
|
697
|
+
if (typeof rawFrom !== "string" || typeof rawTo !== "string" || isLongerThan(rawFrom, DICTATION_MAX_EQUIVALENCE_LENGTH) || isLongerThan(rawTo, DICTATION_MAX_EQUIVALENCE_LENGTH)) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const from = preStripNormalize(rawFrom);
|
|
701
|
+
const to = stripPunctuation(preStripNormalize(rawTo));
|
|
702
|
+
if (from === "" || to === "") {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
rules.push({
|
|
706
|
+
from: phraseOf(from, "none"),
|
|
707
|
+
to,
|
|
708
|
+
spaceBefore: isSpacedWordAt(to, 0),
|
|
709
|
+
spaceAfter: isSpacedWordAt(to, startOfCodePointBefore(to, to.length))
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
rules,
|
|
714
|
+
signature: JSON.stringify(rules.map((rule) => [rule.from.text, rule.to]))
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function bounded(text) {
|
|
718
|
+
return isLongerThan(text, WORKING_LENGTH) ? { text: truncateCodePoints(text, WORKING_LENGTH), cut: true } : { text, cut: false };
|
|
719
|
+
}
|
|
720
|
+
function rewrite(text, rule) {
|
|
721
|
+
let output = "";
|
|
722
|
+
let consumed = 0;
|
|
723
|
+
let last = "";
|
|
724
|
+
for (let at = indexOfPhrase(text, rule.from, 0); at !== -1; at = indexOfPhrase(text, rule.from, consumed)) {
|
|
725
|
+
const between = text.slice(consumed, at);
|
|
726
|
+
if (rule.spaceAfter && consumed > 0 && between !== "" && isSpacedWordAt(between, 0)) {
|
|
727
|
+
output += " ";
|
|
728
|
+
}
|
|
729
|
+
output += between;
|
|
730
|
+
last = lastCodePointAfter(last, between);
|
|
731
|
+
if (rule.spaceBefore && last !== "" && isSpacedWordAt(last, 0)) {
|
|
732
|
+
output += " ";
|
|
733
|
+
last = " ";
|
|
734
|
+
}
|
|
735
|
+
output += rule.to;
|
|
736
|
+
last = lastCodePointAfter(last, rule.to);
|
|
737
|
+
consumed = at + rule.from.text.length;
|
|
738
|
+
if (output.length > 2 * WORKING_LENGTH) {
|
|
739
|
+
return { text: truncateCodePoints(output, WORKING_LENGTH), cut: true };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const rest = text.slice(consumed);
|
|
743
|
+
const apart = rule.spaceAfter && consumed > 0 && rest !== "" && isSpacedWordAt(rest, 0);
|
|
744
|
+
return bounded(apart ? `${output} ${rest}` : output + rest);
|
|
745
|
+
}
|
|
746
|
+
var WORKING_TEXTS = /* @__PURE__ */ Symbol("dictation working texts");
|
|
747
|
+
var MEASURED_TRANSCRIPTS = /* @__PURE__ */ Symbol("dictation measured transcripts");
|
|
748
|
+
var SHOWN_TEXTS = /* @__PURE__ */ Symbol("dictation shown texts");
|
|
749
|
+
var REVEALS = /* @__PURE__ */ Symbol("dictation reveal answers");
|
|
750
|
+
function scopedMap(key, signature) {
|
|
751
|
+
const bySignature = scopeCache(key, () => /* @__PURE__ */ new Map());
|
|
752
|
+
if (bySignature === void 0) {
|
|
753
|
+
return /* @__PURE__ */ new Map();
|
|
754
|
+
}
|
|
755
|
+
let map = bySignature.get(signature);
|
|
756
|
+
if (map === void 0) {
|
|
757
|
+
map = /* @__PURE__ */ new Map();
|
|
758
|
+
bySignature.set(signature, map);
|
|
759
|
+
}
|
|
760
|
+
return map;
|
|
761
|
+
}
|
|
762
|
+
var PRE_STRIP_TEXTS = /* @__PURE__ */ Symbol("dictation pre-strip texts");
|
|
763
|
+
function boundedPreStrip(text) {
|
|
764
|
+
const memo = scopeCache(PRE_STRIP_TEXTS, () => /* @__PURE__ */ new Map());
|
|
765
|
+
let result = memo?.get(text);
|
|
766
|
+
if (result === void 0) {
|
|
767
|
+
result = bounded(preStripNormalize(text));
|
|
768
|
+
memo?.set(text, result);
|
|
769
|
+
}
|
|
770
|
+
return result;
|
|
771
|
+
}
|
|
772
|
+
function isBlankBeforeRules(text) {
|
|
773
|
+
return boundedPreStrip(text).text === "";
|
|
774
|
+
}
|
|
775
|
+
function workingText(text, compiled, preStripped = boundedPreStrip(text)) {
|
|
776
|
+
if (compiled.rules.length === 0) {
|
|
777
|
+
return preStripped;
|
|
778
|
+
}
|
|
779
|
+
const memo = scopeCache(WORKING_TEXTS, () => /* @__PURE__ */ new Map());
|
|
780
|
+
let known = memo?.get(compiled.signature);
|
|
781
|
+
const remembered = known?.get(text);
|
|
782
|
+
if (remembered !== void 0) {
|
|
783
|
+
return remembered;
|
|
784
|
+
}
|
|
785
|
+
let working = preStripped;
|
|
786
|
+
let cut = working.cut;
|
|
787
|
+
for (const rule of compiled.rules) {
|
|
788
|
+
working = rewrite(working.text, rule);
|
|
789
|
+
cut ||= working.cut;
|
|
790
|
+
}
|
|
791
|
+
const result = { text: working.text, cut };
|
|
792
|
+
if (memo !== void 0) {
|
|
793
|
+
if (known === void 0) {
|
|
794
|
+
known = /* @__PURE__ */ new Map();
|
|
795
|
+
memo.set(compiled.signature, known);
|
|
796
|
+
}
|
|
797
|
+
known.set(text, result);
|
|
798
|
+
}
|
|
799
|
+
return result;
|
|
800
|
+
}
|
|
801
|
+
function normalizeDictationTextBounded(text, tolerance) {
|
|
802
|
+
const working = workingText(text, compileRules(tolerance));
|
|
803
|
+
return { text: stripPunctuation(working.text), cut: working.cut };
|
|
804
|
+
}
|
|
805
|
+
function normalizeDictationText(text, tolerance) {
|
|
806
|
+
return normalizeDictationTextBounded(text, tolerance).text;
|
|
807
|
+
}
|
|
808
|
+
var NOTHING_TO_REVEAL = { forms: [], long: false, before: "none", after: "none" };
|
|
809
|
+
function stripForReveal(text) {
|
|
810
|
+
return stripPunctuation(text.replace(DIGIT_SEPARATOR_RE, ""));
|
|
811
|
+
}
|
|
812
|
+
function readingOf(written, working) {
|
|
813
|
+
const writtenStripped = stripForReveal(written);
|
|
814
|
+
const workingStripped = stripForReveal(working);
|
|
815
|
+
const source = writtenStripped === "" ? workingStripped : writtenStripped;
|
|
816
|
+
if (source === "") {
|
|
817
|
+
return NOTHING_TO_REVEAL;
|
|
818
|
+
}
|
|
819
|
+
const long = (source.match(UNSPACED_LETTERS_RE)?.length ?? 0) >= UNSPACED_REVEAL_MIN_CHARACTERS;
|
|
820
|
+
const { before, after } = phraseOf(source, long ? "none" : "visible");
|
|
821
|
+
const forms = [
|
|
822
|
+
writtenStripped,
|
|
823
|
+
spacePunctuation(written),
|
|
824
|
+
workingStripped,
|
|
825
|
+
spacePunctuation(working)
|
|
826
|
+
].map((form) => long ? form.replace(SPACE_BESIDE_UNSPACED_RE, "") : form);
|
|
827
|
+
return { forms: [...new Set(forms)].filter((form) => form !== ""), long, before, after };
|
|
828
|
+
}
|
|
829
|
+
function dictationNormalizer(tolerance) {
|
|
830
|
+
const compiled = compileRules(tolerance);
|
|
831
|
+
const measured = scopedMap(MEASURED_TRANSCRIPTS, compiled.signature);
|
|
832
|
+
const shownTexts = scopedMap(SHOWN_TEXTS, compiled.signature);
|
|
833
|
+
return {
|
|
834
|
+
measure(text) {
|
|
835
|
+
let result = measured.get(text);
|
|
836
|
+
if (result === void 0) {
|
|
837
|
+
if (isLongerThan(text, DICTATION_MAX_TRANSCRIPT_LENGTH)) {
|
|
838
|
+
result = { tooLong: true, normalized: null, reveal: NOTHING_TO_REVEAL };
|
|
839
|
+
} else {
|
|
840
|
+
const written = boundedPreStrip(text);
|
|
841
|
+
const working = workingText(text, compiled, written);
|
|
842
|
+
const normalized = stripPunctuation(working.text);
|
|
843
|
+
result = {
|
|
844
|
+
tooLong: working.cut || isLongerThan(normalized, DICTATION_MAX_TRANSCRIPT_LENGTH),
|
|
845
|
+
normalized,
|
|
846
|
+
reveal: readingOf(written.text, working.text)
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
measured.set(text, result);
|
|
850
|
+
}
|
|
851
|
+
return result;
|
|
852
|
+
},
|
|
853
|
+
shown(text) {
|
|
854
|
+
let result = shownTexts.get(text);
|
|
855
|
+
if (result === void 0) {
|
|
856
|
+
const plainText = isLongerThan(text, WORKING_LENGTH) ? null : boundedPreStrip(text);
|
|
857
|
+
const working = plainText === null ? null : workingText(text, compiled, plainText);
|
|
858
|
+
if (plainText === null || working === null || working.cut) {
|
|
859
|
+
result = { tooLong: true, forms: [] };
|
|
860
|
+
} else {
|
|
861
|
+
const plain = plainText.text;
|
|
862
|
+
result = {
|
|
863
|
+
tooLong: false,
|
|
864
|
+
forms: [
|
|
865
|
+
.../* @__PURE__ */ new Set([
|
|
866
|
+
stripForReveal(working.text),
|
|
867
|
+
spacePunctuation(working.text),
|
|
868
|
+
stripForReveal(plain),
|
|
869
|
+
spacePunctuation(plain)
|
|
870
|
+
])
|
|
871
|
+
]
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
shownTexts.set(text, result);
|
|
875
|
+
}
|
|
876
|
+
return result;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function revealsCandidate(forms, candidate) {
|
|
881
|
+
const memo = scopeCache(REVEALS, () => /* @__PURE__ */ new WeakMap());
|
|
882
|
+
const known = memo?.get(forms)?.get(candidate);
|
|
883
|
+
if (known !== void 0) {
|
|
884
|
+
return known;
|
|
885
|
+
}
|
|
886
|
+
const answer = searchReveal(forms, candidate);
|
|
887
|
+
if (memo !== void 0) {
|
|
888
|
+
const byCandidate = memo.get(forms) ?? /* @__PURE__ */ new WeakMap();
|
|
889
|
+
byCandidate.set(candidate, answer);
|
|
890
|
+
memo.set(forms, byCandidate);
|
|
891
|
+
}
|
|
892
|
+
return answer;
|
|
893
|
+
}
|
|
894
|
+
var COMPACTED_FORMS = /* @__PURE__ */ new WeakMap();
|
|
895
|
+
function searchReveal(forms, candidate) {
|
|
896
|
+
const { reveal } = candidate;
|
|
897
|
+
if (candidate.tooLong) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
let fields = forms;
|
|
901
|
+
if (reveal.long) {
|
|
902
|
+
fields = COMPACTED_FORMS.get(forms) ?? forms.map((form) => form.replace(SPACE_BESIDE_UNSPACED_RE, ""));
|
|
903
|
+
COMPACTED_FORMS.set(forms, fields);
|
|
904
|
+
}
|
|
905
|
+
return reveal.forms.some((text) => {
|
|
906
|
+
const phrase = { text, before: reveal.before, after: reveal.after };
|
|
907
|
+
return fields.some((field) => containsPhrase(field, phrase));
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
41
911
|
// src/schemas/feedback.ts
|
|
42
912
|
import { z } from "zod/v4";
|
|
43
913
|
var FeedbackSchema = z.looseObject({
|
|
@@ -136,47 +1006,264 @@ var RedactedMediaSchema = z2.strictObject({
|
|
|
136
1006
|
path: ["url"]
|
|
137
1007
|
});
|
|
138
1008
|
|
|
139
|
-
// src/schemas/
|
|
1009
|
+
// src/schemas/dictation.ts
|
|
140
1010
|
import { z as z3 } from "zod/v4";
|
|
141
|
-
var
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
normalize: z3.enum(["none", "NFC", "NFKC"]).optional(),
|
|
146
|
-
foldDiacritics: z3.boolean().optional(),
|
|
147
|
-
collapseInnerWhitespace: z3.boolean().optional(),
|
|
148
|
-
ignorePunctuation: z3.boolean().optional(),
|
|
149
|
-
levenshtein: z3.number().int().min(0).optional(),
|
|
150
|
-
locale: z3.string().optional()
|
|
1011
|
+
var DictationSlowMediaSchema = z3.looseObject({
|
|
1012
|
+
type: z3.literal("audio"),
|
|
1013
|
+
url: MediaUrlSchema,
|
|
1014
|
+
alt: z3.string().min(1).optional()
|
|
151
1015
|
});
|
|
152
|
-
var
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
1016
|
+
var RuleTextSchema = z3.string().min(1).meta({ maxLength: DICTATION_MAX_EQUIVALENCE_LENGTH });
|
|
1017
|
+
var TranscriptTextSchema = z3.string().min(1).meta({ maxLength: DICTATION_MAX_TRANSCRIPT_LENGTH });
|
|
1018
|
+
var DictationEquivalenceSchema = z3.looseObject({
|
|
1019
|
+
from: RuleTextSchema,
|
|
1020
|
+
to: RuleTextSchema
|
|
1021
|
+
}).refine((rule) => codePointLength(rule.from) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
|
|
1022
|
+
error: `A rule rewrites at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
|
|
1023
|
+
path: ["from"]
|
|
1024
|
+
}).refine((rule) => codePointLength(rule.to) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
|
|
1025
|
+
error: `A rewrite is at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
|
|
1026
|
+
path: ["to"]
|
|
1027
|
+
});
|
|
1028
|
+
var DictationToleranceSchema = z3.looseObject({
|
|
1029
|
+
equivalences: z3.array(DictationEquivalenceSchema).max(DICTATION_MAX_EQUIVALENCES, {
|
|
1030
|
+
error: `A dictation carries at most ${DICTATION_MAX_EQUIVALENCES} equivalence rules.`
|
|
1031
|
+
}).optional()
|
|
164
1032
|
});
|
|
165
|
-
var
|
|
1033
|
+
var normalizers = /* @__PURE__ */ new WeakMap();
|
|
1034
|
+
function normalizerFor(data) {
|
|
1035
|
+
let normalizer = normalizers.get(data);
|
|
1036
|
+
if (normalizer === void 0) {
|
|
1037
|
+
normalizer = dictationNormalizer(data.tolerance);
|
|
1038
|
+
normalizers.set(data, normalizer);
|
|
1039
|
+
}
|
|
1040
|
+
return normalizer;
|
|
1041
|
+
}
|
|
1042
|
+
function measure(data, text) {
|
|
1043
|
+
return normalizerFor(data).measure(text);
|
|
1044
|
+
}
|
|
1045
|
+
function candidatesOf(data) {
|
|
1046
|
+
return [data.transcript, ...data.acceptedTranscripts ?? []];
|
|
1047
|
+
}
|
|
1048
|
+
function isEmpty(measured) {
|
|
1049
|
+
return measured.normalized === "";
|
|
1050
|
+
}
|
|
1051
|
+
function checkEvenAfterIssues(check) {
|
|
1052
|
+
const guard = new z3.core.$ZodCheck({ check: "custom", when: () => true });
|
|
1053
|
+
guard._zod.check = check;
|
|
1054
|
+
return guard;
|
|
1055
|
+
}
|
|
1056
|
+
var DictationDataShape = z3.looseObject({
|
|
166
1057
|
schemaVersion: z3.literal("1.0"),
|
|
167
|
-
type: z3.literal("
|
|
1058
|
+
type: z3.literal("dictation"),
|
|
168
1059
|
id: z3.string().min(1),
|
|
169
1060
|
title: z3.string().min(1),
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
blanks: z3.array(BlankConfigSchema).min(1),
|
|
173
|
-
scoringStrategy: z3.enum(["all-or-nothing", "partial"]),
|
|
1061
|
+
transcript: TranscriptTextSchema,
|
|
1062
|
+
acceptedTranscripts: z3.array(TranscriptTextSchema).max(DICTATION_MAX_ACCEPTED_TRANSCRIPTS).optional(),
|
|
174
1063
|
media: MediaSchema.optional(),
|
|
1064
|
+
slowMedia: DictationSlowMediaSchema.optional(),
|
|
1065
|
+
hints: z3.looseObject({ mode: z3.literal("progressive-words") }).optional(),
|
|
1066
|
+
tolerance: DictationToleranceSchema.optional(),
|
|
175
1067
|
feedback: FeedbackSchema.optional(),
|
|
176
1068
|
passThreshold: z3.number().min(0).max(1).optional(),
|
|
177
1069
|
locale: z3.string().optional(),
|
|
178
1070
|
learningObjectives: z3.array(z3.string()).optional(),
|
|
179
1071
|
difficultyLevel: z3.literal([1, 2, 3, 4, 5]).optional()
|
|
1072
|
+
});
|
|
1073
|
+
var DictationDataSchema = DictationDataShape.check(
|
|
1074
|
+
checkEvenAfterIssues((ctx) => {
|
|
1075
|
+
if (ctx.issues.some((issue2) => (issue2.path?.length ?? 0) === 0)) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const data = ctx.value;
|
|
1079
|
+
const refused = new Set(ctx.issues.map((issue2) => String(issue2.path?.[0])));
|
|
1080
|
+
const report = (path, input, message) => {
|
|
1081
|
+
ctx.issues.push({ code: "custom", input, message, path: [...path] });
|
|
1082
|
+
};
|
|
1083
|
+
const rulesParse = !refused.has("tolerance");
|
|
1084
|
+
const transcriptParses = !refused.has("transcript");
|
|
1085
|
+
if (rulesParse && transcriptParses) {
|
|
1086
|
+
const transcript = measure(data, data.transcript);
|
|
1087
|
+
if (isEmpty(transcript)) {
|
|
1088
|
+
report(
|
|
1089
|
+
["transcript"],
|
|
1090
|
+
data.transcript,
|
|
1091
|
+
"The transcript must contain something to type: after ignoring case, punctuation and spacing nothing is left."
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
if (transcript.tooLong) {
|
|
1095
|
+
report(
|
|
1096
|
+
["transcript"],
|
|
1097
|
+
data.transcript,
|
|
1098
|
+
`A transcript is at most ${DICTATION_MAX_TRANSCRIPT_LENGTH} characters, before and after its equivalences are applied, and its equivalences may not grow it past ${WORKING_LENGTH} characters on the way.`
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
if (!refused.has("acceptedTranscripts")) {
|
|
1102
|
+
const accepted = data.acceptedTranscripts ?? [];
|
|
1103
|
+
if (accepted.some((entry) => {
|
|
1104
|
+
const measured = measure(data, entry);
|
|
1105
|
+
return measured.tooLong || isEmpty(measured);
|
|
1106
|
+
})) {
|
|
1107
|
+
report(
|
|
1108
|
+
["acceptedTranscripts"],
|
|
1109
|
+
data.acceptedTranscripts,
|
|
1110
|
+
`An accepted transcript must contain something to type and be at most ${DICTATION_MAX_TRANSCRIPT_LENGTH} characters, before and after its equivalences are applied, and its equivalences may not grow it past ${WORKING_LENGTH} characters on the way.`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
const normalized = candidatesOf(data).map((entry) => measure(data, entry).normalized).filter((entry) => entry !== null);
|
|
1114
|
+
if (new Set(normalized).size !== normalized.length) {
|
|
1115
|
+
report(
|
|
1116
|
+
["acceptedTranscripts"],
|
|
1117
|
+
data.acceptedTranscripts,
|
|
1118
|
+
"An accepted transcript must differ from the transcript and from the other accepted transcripts once case, punctuation and spacing are ignored."
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
const mediaParses = !refused.has("media");
|
|
1124
|
+
const slowParses = !refused.has("slowMedia");
|
|
1125
|
+
if (mediaParses && data.media !== void 0 && data.media.type !== "audio") {
|
|
1126
|
+
report(
|
|
1127
|
+
["media", "type"],
|
|
1128
|
+
data.media.type,
|
|
1129
|
+
"A dictation is dictated: its recording must be audio."
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
if (slowParses && data.slowMedia?.playback !== void 0) {
|
|
1133
|
+
report(
|
|
1134
|
+
["slowMedia", "playback"],
|
|
1135
|
+
data.slowMedia.playback,
|
|
1136
|
+
"The slow recording carries no playback policy of its own; put the policy on `media` \u2014 the slow recording follows it (except the play budget)."
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
if (mediaParses && slowParses && data.slowMedia !== void 0) {
|
|
1140
|
+
if (data.media?.playback?.maxPlays !== void 0) {
|
|
1141
|
+
report(
|
|
1142
|
+
["slowMedia"],
|
|
1143
|
+
data.slowMedia,
|
|
1144
|
+
"A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content. Drop `slowMedia`, or drop `maxPlays` (a budgeted recording can still offer the transport's 0.75\xD7 speed when `rate` is `allow`)."
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
if (typeof data.slowMedia.url === "string" && typeof data.media?.url === "string" && data.slowMedia.url === data.media.url) {
|
|
1148
|
+
report(
|
|
1149
|
+
["slowMedia", "url"],
|
|
1150
|
+
data.slowMedia.url,
|
|
1151
|
+
"The slow recording must be a different file from the recording."
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (data.media === void 0) {
|
|
1155
|
+
report(
|
|
1156
|
+
["media"],
|
|
1157
|
+
data.media,
|
|
1158
|
+
"A slow recording accompanies a recording: add `media` first."
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (rulesParse && !(data.tolerance?.equivalences ?? []).every(
|
|
1163
|
+
(rule) => preStripNormalize(rule.from) !== "" && normalizeDictationText(rule.to) !== ""
|
|
1164
|
+
)) {
|
|
1165
|
+
report(
|
|
1166
|
+
["tolerance", "equivalences"],
|
|
1167
|
+
data.tolerance?.equivalences,
|
|
1168
|
+
"An equivalence must name a word, phrase or symbol to rewrite, and rewrite it to something that survives normalisation (not only punctuation or spaces)."
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
for (const field of ["media", "slowMedia"]) {
|
|
1172
|
+
const recording = data[field];
|
|
1173
|
+
if (recording?.captionsUrl !== void 0) {
|
|
1174
|
+
report(
|
|
1175
|
+
[field, "captionsUrl"],
|
|
1176
|
+
recording.captionsUrl,
|
|
1177
|
+
"A dictation recording cannot carry captions: the captions are the answer. Offer an accessible alternative as a different item."
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (!rulesParse || !transcriptParses) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
const normalizer = normalizerFor(data);
|
|
1185
|
+
const candidates = candidatesOf(data).map((entry) => normalizer.measure(entry));
|
|
1186
|
+
const shown = [
|
|
1187
|
+
[["title"], refused.has("title") ? void 0 : data.title],
|
|
1188
|
+
[["media", "alt"], data.media?.alt],
|
|
1189
|
+
[["slowMedia", "alt"], data.slowMedia?.alt]
|
|
1190
|
+
];
|
|
1191
|
+
for (const [path, value] of shown) {
|
|
1192
|
+
if (value === void 0) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const { tooLong, forms } = normalizer.shown(value);
|
|
1196
|
+
if (tooLong) {
|
|
1197
|
+
report(
|
|
1198
|
+
path,
|
|
1199
|
+
value,
|
|
1200
|
+
`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.`
|
|
1201
|
+
);
|
|
1202
|
+
} else if (candidates.some((candidate) => revealsCandidate(forms, candidate))) {
|
|
1203
|
+
report(
|
|
1204
|
+
path,
|
|
1205
|
+
value,
|
|
1206
|
+
'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.'
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
})
|
|
1211
|
+
);
|
|
1212
|
+
function groupCaptionsRevealDictation(group, hasCaptions = (captionsUrl) => captionsUrl !== void 0) {
|
|
1213
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1214
|
+
if (!isObject(group) || !isObject(group.stimulus) || !isObject(group.stimulus.media)) {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
if (!hasCaptions(group.stimulus.media.captionsUrl) || !Array.isArray(group.items)) {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
return group.items.some(
|
|
1221
|
+
(item) => isObject(item) && item.type === "dictation" && item.media === void 0
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
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.";
|
|
1225
|
+
|
|
1226
|
+
// src/schemas/fill-in-the-blanks.ts
|
|
1227
|
+
import { z as z4 } from "zod/v4";
|
|
1228
|
+
var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
1229
|
+
var TextMatchPolicySchema = z4.looseObject({
|
|
1230
|
+
caseSensitive: z4.boolean().optional(),
|
|
1231
|
+
trim: z4.boolean().optional(),
|
|
1232
|
+
normalize: z4.enum(["none", "NFC", "NFKC"]).optional(),
|
|
1233
|
+
foldDiacritics: z4.boolean().optional(),
|
|
1234
|
+
collapseInnerWhitespace: z4.boolean().optional(),
|
|
1235
|
+
ignorePunctuation: z4.boolean().optional(),
|
|
1236
|
+
levenshtein: z4.number().int().min(0).optional(),
|
|
1237
|
+
locale: z4.string().optional()
|
|
1238
|
+
});
|
|
1239
|
+
var BlankConfigSchema = z4.looseObject({
|
|
1240
|
+
id: z4.string().min(1),
|
|
1241
|
+
acceptedAnswers: z4.array(
|
|
1242
|
+
z4.string().min(1).refine((answer) => answer.trim().length > 0, {
|
|
1243
|
+
error: "Accepted answers must contain non-whitespace characters."
|
|
1244
|
+
})
|
|
1245
|
+
).min(1),
|
|
1246
|
+
caseSensitive: z4.boolean().optional(),
|
|
1247
|
+
trimWhitespace: z4.boolean().optional(),
|
|
1248
|
+
match: TextMatchPolicySchema.optional(),
|
|
1249
|
+
hint: z4.string().optional(),
|
|
1250
|
+
feedback: z4.string().optional()
|
|
1251
|
+
});
|
|
1252
|
+
var FillInTheBlanksDataSchema = z4.looseObject({
|
|
1253
|
+
schemaVersion: z4.literal("1.0"),
|
|
1254
|
+
type: z4.literal("fill-in-the-blanks"),
|
|
1255
|
+
id: z4.string().min(1),
|
|
1256
|
+
title: z4.string().min(1),
|
|
1257
|
+
passage: z4.string().min(1),
|
|
1258
|
+
passageHtml: z4.string().optional(),
|
|
1259
|
+
blanks: z4.array(BlankConfigSchema).min(1),
|
|
1260
|
+
scoringStrategy: z4.enum(["all-or-nothing", "partial"]),
|
|
1261
|
+
media: MediaSchema.optional(),
|
|
1262
|
+
feedback: FeedbackSchema.optional(),
|
|
1263
|
+
passThreshold: z4.number().min(0).max(1).optional(),
|
|
1264
|
+
locale: z4.string().optional(),
|
|
1265
|
+
learningObjectives: z4.array(z4.string()).optional(),
|
|
1266
|
+
difficultyLevel: z4.literal([1, 2, 3, 4, 5]).optional()
|
|
180
1267
|
}).refine(
|
|
181
1268
|
(data) => {
|
|
182
1269
|
const placeholderCounts2 = /* @__PURE__ */ new Map();
|
|
@@ -200,14 +1287,14 @@ var FillInTheBlanksDataSchema = z3.looseObject({
|
|
|
200
1287
|
);
|
|
201
1288
|
|
|
202
1289
|
// src/schemas/multiple-choice.ts
|
|
203
|
-
import { z as
|
|
204
|
-
var MultipleChoiceOptionMediaSchema =
|
|
1290
|
+
import { z as z5 } from "zod/v4";
|
|
1291
|
+
var MultipleChoiceOptionMediaSchema = z5.looseObject({
|
|
205
1292
|
// No `embed`: an iframe swallows the click that selects the option, so the
|
|
206
1293
|
// learner could not choose it. No `video`: a native control bar inside the
|
|
207
1294
|
// option's label eats the same click.
|
|
208
|
-
type:
|
|
1295
|
+
type: z5.enum(["image", "audio"]),
|
|
209
1296
|
url: MediaUrlSchema,
|
|
210
|
-
alt:
|
|
1297
|
+
alt: z5.string().min(1).optional(),
|
|
211
1298
|
captionsUrl: MediaUrlSchema.optional()
|
|
212
1299
|
}).refine((media) => media.type !== "image" || media.alt !== void 0 && media.alt !== "", {
|
|
213
1300
|
error: "An image option requires a non-empty `alt`.",
|
|
@@ -216,30 +1303,30 @@ var MultipleChoiceOptionMediaSchema = z4.looseObject({
|
|
|
216
1303
|
error: "An option carries no playback policy. maxPlays, seek and rate are enforced only on the media above the question.",
|
|
217
1304
|
path: ["playback"]
|
|
218
1305
|
});
|
|
219
|
-
var MultipleChoiceOptionSchema =
|
|
220
|
-
id:
|
|
221
|
-
text:
|
|
222
|
-
isCorrect:
|
|
223
|
-
feedback:
|
|
1306
|
+
var MultipleChoiceOptionSchema = z5.looseObject({
|
|
1307
|
+
id: z5.string().min(1),
|
|
1308
|
+
text: z5.string().min(1),
|
|
1309
|
+
isCorrect: z5.boolean(),
|
|
1310
|
+
feedback: z5.string().optional(),
|
|
224
1311
|
media: MultipleChoiceOptionMediaSchema.optional()
|
|
225
1312
|
});
|
|
226
|
-
var MultipleChoiceDataSchema =
|
|
227
|
-
schemaVersion:
|
|
228
|
-
type:
|
|
229
|
-
id:
|
|
230
|
-
title:
|
|
231
|
-
question:
|
|
232
|
-
questionHtml:
|
|
233
|
-
mode:
|
|
234
|
-
options:
|
|
235
|
-
scoringStrategy:
|
|
1313
|
+
var MultipleChoiceDataSchema = z5.looseObject({
|
|
1314
|
+
schemaVersion: z5.literal("1.0"),
|
|
1315
|
+
type: z5.literal("multiple-choice"),
|
|
1316
|
+
id: z5.string().min(1),
|
|
1317
|
+
title: z5.string().min(1),
|
|
1318
|
+
question: z5.string().min(1),
|
|
1319
|
+
questionHtml: z5.string().optional(),
|
|
1320
|
+
mode: z5.enum(["single", "multi"]),
|
|
1321
|
+
options: z5.array(MultipleChoiceOptionSchema).min(2).max(26),
|
|
1322
|
+
scoringStrategy: z5.enum(["all-or-nothing", "partial"]),
|
|
236
1323
|
media: MediaSchema.optional(),
|
|
237
1324
|
feedback: FeedbackSchema.optional(),
|
|
238
|
-
passThreshold:
|
|
239
|
-
shuffle:
|
|
240
|
-
locale:
|
|
241
|
-
learningObjectives:
|
|
242
|
-
difficultyLevel:
|
|
1325
|
+
passThreshold: z5.number().min(0).max(1).optional(),
|
|
1326
|
+
shuffle: z5.boolean().optional(),
|
|
1327
|
+
locale: z5.string().optional(),
|
|
1328
|
+
learningObjectives: z5.array(z5.string()).optional(),
|
|
1329
|
+
difficultyLevel: z5.literal([1, 2, 3, 4, 5]).optional()
|
|
243
1330
|
}).refine((data) => data.options.some((option) => option.isCorrect), {
|
|
244
1331
|
error: "At least one option must be marked correct.",
|
|
245
1332
|
path: ["options"]
|
|
@@ -267,21 +1354,21 @@ function countWords(text) {
|
|
|
267
1354
|
}
|
|
268
1355
|
|
|
269
1356
|
// src/schemas/gap-select.ts
|
|
270
|
-
import { z as
|
|
271
|
-
var GapSelectChoiceSchema =
|
|
272
|
-
id:
|
|
273
|
-
text:
|
|
1357
|
+
import { z as z6 } from "zod/v4";
|
|
1358
|
+
var GapSelectChoiceSchema = z6.looseObject({
|
|
1359
|
+
id: z6.string().min(1),
|
|
1360
|
+
text: z6.string().min(1)
|
|
274
1361
|
});
|
|
275
|
-
var GapSelectBankSchema =
|
|
276
|
-
id:
|
|
277
|
-
choices:
|
|
1362
|
+
var GapSelectBankSchema = z6.looseObject({
|
|
1363
|
+
id: z6.string().min(1),
|
|
1364
|
+
choices: z6.array(GapSelectChoiceSchema).min(2)
|
|
278
1365
|
});
|
|
279
|
-
var GapSelectGapSchema =
|
|
280
|
-
id:
|
|
281
|
-
choices:
|
|
282
|
-
bankId:
|
|
283
|
-
correctChoiceId:
|
|
284
|
-
feedback:
|
|
1366
|
+
var GapSelectGapSchema = z6.looseObject({
|
|
1367
|
+
id: z6.string().min(1),
|
|
1368
|
+
choices: z6.array(GapSelectChoiceSchema).min(2).optional(),
|
|
1369
|
+
bankId: z6.string().min(1).optional(),
|
|
1370
|
+
correctChoiceId: z6.string().min(1),
|
|
1371
|
+
feedback: z6.string().optional()
|
|
285
1372
|
});
|
|
286
1373
|
function resolveChoices(gap, banks) {
|
|
287
1374
|
if (gap.choices !== void 0) {
|
|
@@ -300,24 +1387,24 @@ function placeholderCounts(passage) {
|
|
|
300
1387
|
}
|
|
301
1388
|
return counts;
|
|
302
1389
|
}
|
|
303
|
-
var GapSelectDataSchema =
|
|
304
|
-
schemaVersion:
|
|
305
|
-
type:
|
|
306
|
-
id:
|
|
307
|
-
title:
|
|
308
|
-
passage:
|
|
309
|
-
passageHtml:
|
|
310
|
-
gaps:
|
|
311
|
-
banks:
|
|
312
|
-
scoringStrategy:
|
|
313
|
-
presentation:
|
|
314
|
-
shuffleChoices:
|
|
1390
|
+
var GapSelectDataSchema = z6.looseObject({
|
|
1391
|
+
schemaVersion: z6.literal("1.0"),
|
|
1392
|
+
type: z6.literal("gap-select"),
|
|
1393
|
+
id: z6.string().min(1),
|
|
1394
|
+
title: z6.string().min(1),
|
|
1395
|
+
passage: z6.string().min(1),
|
|
1396
|
+
passageHtml: z6.string().optional(),
|
|
1397
|
+
gaps: z6.array(GapSelectGapSchema).min(1),
|
|
1398
|
+
banks: z6.array(GapSelectBankSchema).optional(),
|
|
1399
|
+
scoringStrategy: z6.enum(["all-or-nothing", "partial"]),
|
|
1400
|
+
presentation: z6.literal("dropdown").optional(),
|
|
1401
|
+
shuffleChoices: z6.boolean().optional(),
|
|
315
1402
|
media: MediaSchema.optional(),
|
|
316
1403
|
feedback: FeedbackSchema.optional(),
|
|
317
|
-
passThreshold:
|
|
318
|
-
locale:
|
|
319
|
-
learningObjectives:
|
|
320
|
-
difficultyLevel:
|
|
1404
|
+
passThreshold: z6.number().min(0).max(1).optional(),
|
|
1405
|
+
locale: z6.string().optional(),
|
|
1406
|
+
learningObjectives: z6.array(z6.string()).optional(),
|
|
1407
|
+
difficultyLevel: z6.literal([1, 2, 3, 4, 5]).optional()
|
|
321
1408
|
}).refine(
|
|
322
1409
|
(data) => {
|
|
323
1410
|
const ids = (data.banks ?? []).map((bank) => bank.id);
|
|
@@ -371,131 +1458,359 @@ var GapSelectDataSchema = z5.looseObject({
|
|
|
371
1458
|
);
|
|
372
1459
|
|
|
373
1460
|
// src/schemas/written-response.ts
|
|
374
|
-
import { z as
|
|
375
|
-
var WrittenResponseRubricCriterionSchema =
|
|
376
|
-
name:
|
|
377
|
-
description:
|
|
378
|
-
weight:
|
|
1461
|
+
import { z as z7 } from "zod/v4";
|
|
1462
|
+
var WrittenResponseRubricCriterionSchema = z7.looseObject({
|
|
1463
|
+
name: z7.string().min(1),
|
|
1464
|
+
description: z7.string().optional(),
|
|
1465
|
+
weight: z7.number().min(0)
|
|
379
1466
|
});
|
|
380
|
-
var WrittenResponseRubricSchema =
|
|
381
|
-
label:
|
|
382
|
-
criteria:
|
|
1467
|
+
var WrittenResponseRubricSchema = z7.looseObject({
|
|
1468
|
+
label: z7.string().optional(),
|
|
1469
|
+
criteria: z7.array(WrittenResponseRubricCriterionSchema).min(1)
|
|
383
1470
|
});
|
|
384
|
-
var RedactedWrittenResponseRubricCriterionSchema =
|
|
385
|
-
name:
|
|
386
|
-
description:
|
|
387
|
-
weight:
|
|
1471
|
+
var RedactedWrittenResponseRubricCriterionSchema = z7.strictObject({
|
|
1472
|
+
name: z7.string().min(1),
|
|
1473
|
+
description: z7.string().optional(),
|
|
1474
|
+
weight: z7.number().min(0)
|
|
388
1475
|
});
|
|
389
|
-
var RedactedWrittenResponseRubricSchema =
|
|
390
|
-
label:
|
|
391
|
-
criteria:
|
|
1476
|
+
var RedactedWrittenResponseRubricSchema = z7.strictObject({
|
|
1477
|
+
label: z7.string().optional(),
|
|
1478
|
+
criteria: z7.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
|
|
392
1479
|
});
|
|
393
|
-
var WrittenResponseDataSchema =
|
|
394
|
-
schemaVersion:
|
|
395
|
-
type:
|
|
396
|
-
id:
|
|
397
|
-
title:
|
|
398
|
-
prompt:
|
|
399
|
-
promptHtml:
|
|
400
|
-
minWords:
|
|
401
|
-
maxWords:
|
|
1480
|
+
var WrittenResponseDataSchema = z7.looseObject({
|
|
1481
|
+
schemaVersion: z7.literal("1.0"),
|
|
1482
|
+
type: z7.literal("written-response"),
|
|
1483
|
+
id: z7.string().min(1),
|
|
1484
|
+
title: z7.string().min(1),
|
|
1485
|
+
prompt: z7.string(),
|
|
1486
|
+
promptHtml: z7.string().optional(),
|
|
1487
|
+
minWords: z7.number().int().min(0),
|
|
1488
|
+
maxWords: z7.number().int().min(1),
|
|
402
1489
|
rubric: WrittenResponseRubricSchema.optional(),
|
|
403
|
-
languageTarget:
|
|
1490
|
+
languageTarget: z7.string().optional(),
|
|
404
1491
|
media: MediaSchema.optional(),
|
|
405
1492
|
feedback: FeedbackSchema.optional(),
|
|
406
|
-
passThreshold:
|
|
407
|
-
locale:
|
|
408
|
-
learningObjectives:
|
|
409
|
-
difficultyLevel:
|
|
1493
|
+
passThreshold: z7.number().min(0).max(1).optional(),
|
|
1494
|
+
locale: z7.string().optional(),
|
|
1495
|
+
learningObjectives: z7.array(z7.string()).optional(),
|
|
1496
|
+
difficultyLevel: z7.literal([1, 2, 3, 4, 5]).optional()
|
|
410
1497
|
}).refine((data) => data.maxWords >= data.minWords, {
|
|
411
1498
|
error: "maxWords must be greater than or equal to minWords.",
|
|
412
1499
|
path: ["maxWords"]
|
|
413
1500
|
});
|
|
414
1501
|
|
|
415
1502
|
// src/schemas/redacted.ts
|
|
416
|
-
import { z as
|
|
1503
|
+
import { z as z8 } from "zod/v4";
|
|
417
1504
|
var redactedBase = {
|
|
418
|
-
/** Marker distinguishing a redacted projection from full activity data. */
|
|
419
|
-
redacted:
|
|
420
|
-
/** Slot identity, carried through redaction so the client and the plan agree. */
|
|
421
|
-
slotKey:
|
|
422
|
-
schemaVersion:
|
|
423
|
-
id:
|
|
424
|
-
title:
|
|
1505
|
+
/** Marker distinguishing a redacted projection from full activity data. */
|
|
1506
|
+
redacted: z8.literal(true),
|
|
1507
|
+
/** Slot identity, carried through redaction so the client and the plan agree. */
|
|
1508
|
+
slotKey: z8.string().min(1).optional(),
|
|
1509
|
+
schemaVersion: z8.literal("1.0"),
|
|
1510
|
+
id: z8.string().min(1),
|
|
1511
|
+
title: z8.string().min(1),
|
|
425
1512
|
media: RedactedMediaSchema.optional(),
|
|
426
|
-
passThreshold:
|
|
427
|
-
locale:
|
|
428
|
-
learningObjectives:
|
|
429
|
-
difficultyLevel:
|
|
1513
|
+
passThreshold: z8.number().min(0).max(1).optional(),
|
|
1514
|
+
locale: z8.string().optional(),
|
|
1515
|
+
learningObjectives: z8.array(z8.string()).optional(),
|
|
1516
|
+
difficultyLevel: z8.literal([1, 2, 3, 4, 5]).optional()
|
|
430
1517
|
};
|
|
431
|
-
var RedactedMultipleChoiceOptionMediaSchema =
|
|
432
|
-
type:
|
|
433
|
-
url:
|
|
434
|
-
alt:
|
|
435
|
-
captionsUrl:
|
|
1518
|
+
var RedactedMultipleChoiceOptionMediaSchema = z8.strictObject({
|
|
1519
|
+
type: z8.enum(["image", "audio"]),
|
|
1520
|
+
url: z8.string().min(1),
|
|
1521
|
+
alt: z8.string().min(1).optional(),
|
|
1522
|
+
captionsUrl: z8.string().min(1).optional()
|
|
436
1523
|
});
|
|
437
|
-
var RedactedMultipleChoiceOptionSchema =
|
|
438
|
-
id:
|
|
439
|
-
text:
|
|
1524
|
+
var RedactedMultipleChoiceOptionSchema = z8.strictObject({
|
|
1525
|
+
id: z8.string().min(1),
|
|
1526
|
+
text: z8.string().min(1),
|
|
440
1527
|
media: RedactedMultipleChoiceOptionMediaSchema.optional()
|
|
441
1528
|
});
|
|
442
|
-
var RedactedMultipleChoiceDataSchema =
|
|
1529
|
+
var RedactedMultipleChoiceDataSchema = z8.strictObject({
|
|
443
1530
|
...redactedBase,
|
|
444
|
-
type:
|
|
445
|
-
question:
|
|
446
|
-
questionHtml:
|
|
447
|
-
mode:
|
|
448
|
-
options:
|
|
449
|
-
shuffle:
|
|
1531
|
+
type: z8.literal("multiple-choice"),
|
|
1532
|
+
question: z8.string().min(1),
|
|
1533
|
+
questionHtml: z8.string().optional(),
|
|
1534
|
+
mode: z8.enum(["single", "multi"]),
|
|
1535
|
+
options: z8.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
|
|
1536
|
+
shuffle: z8.boolean().optional()
|
|
450
1537
|
});
|
|
451
|
-
var RedactedBlankConfigSchema =
|
|
452
|
-
id:
|
|
453
|
-
hint:
|
|
1538
|
+
var RedactedBlankConfigSchema = z8.strictObject({
|
|
1539
|
+
id: z8.string().min(1),
|
|
1540
|
+
hint: z8.string().optional()
|
|
454
1541
|
});
|
|
455
|
-
var RedactedFillInTheBlanksDataSchema =
|
|
1542
|
+
var RedactedFillInTheBlanksDataSchema = z8.strictObject({
|
|
456
1543
|
...redactedBase,
|
|
457
|
-
type:
|
|
458
|
-
passage:
|
|
459
|
-
passageHtml:
|
|
460
|
-
blanks:
|
|
1544
|
+
type: z8.literal("fill-in-the-blanks"),
|
|
1545
|
+
passage: z8.string().min(1),
|
|
1546
|
+
passageHtml: z8.string().optional(),
|
|
1547
|
+
blanks: z8.array(RedactedBlankConfigSchema).min(1)
|
|
461
1548
|
});
|
|
462
|
-
var RedactedGapSelectChoiceSchema =
|
|
463
|
-
id:
|
|
464
|
-
text:
|
|
1549
|
+
var RedactedGapSelectChoiceSchema = z8.strictObject({
|
|
1550
|
+
id: z8.string().min(1),
|
|
1551
|
+
text: z8.string().min(1)
|
|
465
1552
|
});
|
|
466
|
-
var RedactedGapSelectBankSchema =
|
|
467
|
-
id:
|
|
468
|
-
choices:
|
|
1553
|
+
var RedactedGapSelectBankSchema = z8.strictObject({
|
|
1554
|
+
id: z8.string().min(1),
|
|
1555
|
+
choices: z8.array(RedactedGapSelectChoiceSchema).min(2)
|
|
469
1556
|
});
|
|
470
|
-
var RedactedGapSelectGapSchema =
|
|
471
|
-
id:
|
|
472
|
-
choices:
|
|
473
|
-
bankId:
|
|
1557
|
+
var RedactedGapSelectGapSchema = z8.strictObject({
|
|
1558
|
+
id: z8.string().min(1),
|
|
1559
|
+
choices: z8.array(RedactedGapSelectChoiceSchema).min(2).optional(),
|
|
1560
|
+
bankId: z8.string().min(1).optional()
|
|
474
1561
|
});
|
|
475
|
-
var RedactedGapSelectDataSchema =
|
|
1562
|
+
var RedactedGapSelectDataSchema = z8.strictObject({
|
|
476
1563
|
...redactedBase,
|
|
477
|
-
type:
|
|
478
|
-
passage:
|
|
479
|
-
passageHtml:
|
|
480
|
-
gaps:
|
|
481
|
-
banks:
|
|
482
|
-
presentation:
|
|
483
|
-
shuffleChoices:
|
|
1564
|
+
type: z8.literal("gap-select"),
|
|
1565
|
+
passage: z8.string().min(1),
|
|
1566
|
+
passageHtml: z8.string().optional(),
|
|
1567
|
+
gaps: z8.array(RedactedGapSelectGapSchema).min(1),
|
|
1568
|
+
banks: z8.array(RedactedGapSelectBankSchema).optional(),
|
|
1569
|
+
presentation: z8.literal("dropdown").optional(),
|
|
1570
|
+
shuffleChoices: z8.boolean().optional()
|
|
484
1571
|
});
|
|
485
|
-
var RedactedWrittenResponseDataSchema =
|
|
1572
|
+
var RedactedWrittenResponseDataSchema = z8.strictObject({
|
|
486
1573
|
...redactedBase,
|
|
487
|
-
type:
|
|
488
|
-
prompt:
|
|
489
|
-
promptHtml:
|
|
490
|
-
minWords:
|
|
491
|
-
maxWords:
|
|
1574
|
+
type: z8.literal("written-response"),
|
|
1575
|
+
prompt: z8.string(),
|
|
1576
|
+
promptHtml: z8.string().optional(),
|
|
1577
|
+
minWords: z8.number().int().min(0),
|
|
1578
|
+
maxWords: z8.number().int().min(1),
|
|
492
1579
|
rubric: RedactedWrittenResponseRubricSchema.optional(),
|
|
493
|
-
languageTarget:
|
|
1580
|
+
languageTarget: z8.string().optional()
|
|
494
1581
|
});
|
|
1582
|
+
var RedactedDictationSlowMediaSchema = z8.strictObject({
|
|
1583
|
+
type: z8.literal("audio"),
|
|
1584
|
+
url: MediaUrlSchema,
|
|
1585
|
+
alt: z8.string().min(1).optional()
|
|
1586
|
+
});
|
|
1587
|
+
var RedactedDictationDataSchema = z8.strictObject({
|
|
1588
|
+
...redactedBase,
|
|
1589
|
+
type: z8.literal("dictation"),
|
|
1590
|
+
slowMedia: RedactedDictationSlowMediaSchema.optional(),
|
|
1591
|
+
hints: z8.strictObject({ mode: z8.literal("progressive-words") }).optional()
|
|
1592
|
+
}).check((ctx) => {
|
|
1593
|
+
const data = ctx.value;
|
|
1594
|
+
if (data.media?.captionsUrl !== void 0) {
|
|
1595
|
+
ctx.issues.push({
|
|
1596
|
+
code: "custom",
|
|
1597
|
+
input: data.media.captionsUrl,
|
|
1598
|
+
message: "A dictation recording cannot carry captions: the captions are the answer, so this payload is not learner-safe.",
|
|
1599
|
+
path: ["media", "captionsUrl"]
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
if (data.slowMedia !== void 0 && data.media?.playback?.maxPlays !== void 0) {
|
|
1603
|
+
ctx.issues.push({
|
|
1604
|
+
code: "custom",
|
|
1605
|
+
input: data.slowMedia,
|
|
1606
|
+
message: "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content.",
|
|
1607
|
+
path: ["slowMedia"]
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
if (data.slowMedia !== void 0 && data.media === void 0) {
|
|
1611
|
+
ctx.issues.push({
|
|
1612
|
+
code: "custom",
|
|
1613
|
+
input: data.slowMedia,
|
|
1614
|
+
message: "A slow recording accompanies a recording: this payload has no `media`.",
|
|
1615
|
+
path: ["media"]
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
// src/scoring/dictation/edit-distance.ts
|
|
1621
|
+
function codePoints(text) {
|
|
1622
|
+
return Array.from(text, (character) => character.codePointAt(0));
|
|
1623
|
+
}
|
|
1624
|
+
function editDistance(a, b) {
|
|
1625
|
+
if (a === b) {
|
|
1626
|
+
return 0;
|
|
1627
|
+
}
|
|
1628
|
+
let left = codePoints(a);
|
|
1629
|
+
let right = codePoints(b);
|
|
1630
|
+
if (left.length < right.length) {
|
|
1631
|
+
[left, right] = [right, left];
|
|
1632
|
+
}
|
|
1633
|
+
if (right.length === 0) {
|
|
1634
|
+
return left.length;
|
|
1635
|
+
}
|
|
1636
|
+
let previous = new Uint32Array(right.length + 1);
|
|
1637
|
+
let current = new Uint32Array(right.length + 1);
|
|
1638
|
+
for (let j = 0; j <= right.length; j += 1) {
|
|
1639
|
+
previous[j] = j;
|
|
1640
|
+
}
|
|
1641
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
1642
|
+
current[0] = i;
|
|
1643
|
+
const leftPoint = left[i - 1];
|
|
1644
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
1645
|
+
const deletion = previous[j] + 1;
|
|
1646
|
+
const insertion = current[j - 1] + 1;
|
|
1647
|
+
const substitution = previous[j - 1] + (leftPoint === right[j - 1] ? 0 : 1);
|
|
1648
|
+
current[j] = Math.min(deletion, insertion, substitution);
|
|
1649
|
+
}
|
|
1650
|
+
[previous, current] = [current, previous];
|
|
1651
|
+
}
|
|
1652
|
+
return previous[right.length];
|
|
1653
|
+
}
|
|
1654
|
+
function similarity(a, b) {
|
|
1655
|
+
const max = Math.max(codePoints(a).length, codePoints(b).length);
|
|
1656
|
+
return max === 0 ? 0 : (max - editDistance(a, b)) / max;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/scoring/dictation/align.ts
|
|
1660
|
+
function dictationReferenceWords(data) {
|
|
1661
|
+
return rawCandidates(data).map(
|
|
1662
|
+
(candidate) => tokensOf(normalizeCandidate(candidate, data)).map((word, index) => ({
|
|
1663
|
+
itemId: `w${index + 1}`,
|
|
1664
|
+
word
|
|
1665
|
+
}))
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
function alignDictation(data, text) {
|
|
1669
|
+
const raw = typeof text === "string" ? text : "";
|
|
1670
|
+
const capped = truncateCodePoints(raw, DICTATION_MAX_TEXT_LENGTH);
|
|
1671
|
+
const { text: normalized, cut } = normalizeDictationTextBounded(capped, data.tolerance);
|
|
1672
|
+
const attempt = cutAt(normalized, DICTATION_MAX_TEXT_LENGTH);
|
|
1673
|
+
const truncated = cut || capped.length < raw.length || attempt.length < normalized.length;
|
|
1674
|
+
const candidates = [];
|
|
1675
|
+
rawCandidates(data).forEach((candidate, index) => {
|
|
1676
|
+
const candidateText = normalizeCandidate(candidate, data);
|
|
1677
|
+
if (candidateText !== "") {
|
|
1678
|
+
candidates.push({ index, text: candidateText });
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
if (candidates.length === 0) {
|
|
1682
|
+
return { candidateIndex: -1, reference: "", attempt, truncated, similarity: 0, words: [] };
|
|
1683
|
+
}
|
|
1684
|
+
let chosen = candidates[0];
|
|
1685
|
+
let best = similarity(chosen.text, attempt);
|
|
1686
|
+
for (const candidate of candidates.slice(1)) {
|
|
1687
|
+
const candidateSimilarity = similarity(candidate.text, attempt);
|
|
1688
|
+
if (candidateSimilarity > best) {
|
|
1689
|
+
best = candidateSimilarity;
|
|
1690
|
+
chosen = candidate;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return {
|
|
1694
|
+
candidateIndex: chosen.index,
|
|
1695
|
+
reference: chosen.text,
|
|
1696
|
+
attempt,
|
|
1697
|
+
truncated,
|
|
1698
|
+
similarity: best,
|
|
1699
|
+
words: alignWords(tokensOf(chosen.text), tokensOf(attempt))
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
function rawCandidates(data) {
|
|
1703
|
+
return [
|
|
1704
|
+
data.transcript,
|
|
1705
|
+
...Array.isArray(data.acceptedTranscripts) ? data.acceptedTranscripts.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS) : []
|
|
1706
|
+
];
|
|
1707
|
+
}
|
|
1708
|
+
function normalizeCandidate(candidate, data) {
|
|
1709
|
+
return typeof candidate === "string" ? cutAt(
|
|
1710
|
+
normalizeDictationText(
|
|
1711
|
+
truncateCodePoints(candidate, DICTATION_MAX_TRANSCRIPT_LENGTH),
|
|
1712
|
+
data.tolerance
|
|
1713
|
+
),
|
|
1714
|
+
DICTATION_MAX_TRANSCRIPT_LENGTH
|
|
1715
|
+
) : "";
|
|
1716
|
+
}
|
|
1717
|
+
function cutAt(normalized, max) {
|
|
1718
|
+
const cut = truncateCodePoints(normalized, max);
|
|
1719
|
+
return cut.endsWith(" ") ? cut.slice(0, -1) : cut;
|
|
1720
|
+
}
|
|
1721
|
+
function tokensOf(normalized) {
|
|
1722
|
+
return normalized === "" ? [] : normalized.split(" ");
|
|
1723
|
+
}
|
|
1724
|
+
var STEP = {
|
|
1725
|
+
/** A reference element paired with an attempt element — equal or substituted. */
|
|
1726
|
+
pair: 0,
|
|
1727
|
+
/** A reference element the attempt lacks. */
|
|
1728
|
+
missing: 1,
|
|
1729
|
+
/** An attempt element the reference lacks. */
|
|
1730
|
+
extra: 2
|
|
1731
|
+
};
|
|
1732
|
+
function alignSequences(reference, attempt) {
|
|
1733
|
+
const rows = reference.length;
|
|
1734
|
+
const columns = attempt.length;
|
|
1735
|
+
const width = columns + 1;
|
|
1736
|
+
const steps = new Uint8Array((rows + 1) * width);
|
|
1737
|
+
let previous = new Uint32Array(width);
|
|
1738
|
+
let current = new Uint32Array(width);
|
|
1739
|
+
for (let j2 = 0; j2 <= columns; j2 += 1) {
|
|
1740
|
+
previous[j2] = j2;
|
|
1741
|
+
steps[j2] = STEP.extra;
|
|
1742
|
+
}
|
|
1743
|
+
for (let i2 = 1; i2 <= rows; i2 += 1) {
|
|
1744
|
+
current[0] = i2;
|
|
1745
|
+
steps[i2 * width] = STEP.missing;
|
|
1746
|
+
const referenceItem = reference[i2 - 1];
|
|
1747
|
+
for (let j2 = 1; j2 <= columns; j2 += 1) {
|
|
1748
|
+
const paired = previous[j2 - 1] + (referenceItem === attempt[j2 - 1] ? 0 : 1);
|
|
1749
|
+
const missing = previous[j2] + 1;
|
|
1750
|
+
const extra = current[j2 - 1] + 1;
|
|
1751
|
+
const cost = Math.min(paired, missing, extra);
|
|
1752
|
+
current[j2] = cost;
|
|
1753
|
+
steps[i2 * width + j2] = paired === cost ? STEP.pair : missing === cost ? STEP.missing : STEP.extra;
|
|
1754
|
+
}
|
|
1755
|
+
[previous, current] = [current, previous];
|
|
1756
|
+
}
|
|
1757
|
+
const path = [];
|
|
1758
|
+
let i = rows;
|
|
1759
|
+
let j = columns;
|
|
1760
|
+
while (i > 0 || j > 0) {
|
|
1761
|
+
const step = steps[i * width + j];
|
|
1762
|
+
if (step === STEP.pair) {
|
|
1763
|
+
i -= 1;
|
|
1764
|
+
j -= 1;
|
|
1765
|
+
} else if (step === STEP.missing) {
|
|
1766
|
+
i -= 1;
|
|
1767
|
+
} else {
|
|
1768
|
+
j -= 1;
|
|
1769
|
+
}
|
|
1770
|
+
path.push({ step, referenceIndex: i, attemptIndex: j });
|
|
1771
|
+
}
|
|
1772
|
+
return path.reverse();
|
|
1773
|
+
}
|
|
1774
|
+
function alignWords(reference, attempt) {
|
|
1775
|
+
const words = [];
|
|
1776
|
+
let ordinal = 0;
|
|
1777
|
+
for (const { step, referenceIndex, attemptIndex } of alignSequences(reference, attempt)) {
|
|
1778
|
+
if (step === STEP.extra) {
|
|
1779
|
+
words.push({
|
|
1780
|
+
reference: "",
|
|
1781
|
+
attempt: attempt[attemptIndex],
|
|
1782
|
+
similarity: 0,
|
|
1783
|
+
status: "extra"
|
|
1784
|
+
});
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
ordinal += 1;
|
|
1788
|
+
const referenceWord = reference[referenceIndex];
|
|
1789
|
+
if (step === STEP.missing) {
|
|
1790
|
+
words.push({
|
|
1791
|
+
itemId: `w${ordinal}`,
|
|
1792
|
+
reference: referenceWord,
|
|
1793
|
+
attempt: "",
|
|
1794
|
+
similarity: 0,
|
|
1795
|
+
status: "missing"
|
|
1796
|
+
});
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
const attemptWord = attempt[attemptIndex];
|
|
1800
|
+
words.push({
|
|
1801
|
+
itemId: `w${ordinal}`,
|
|
1802
|
+
reference: referenceWord,
|
|
1803
|
+
attempt: attemptWord,
|
|
1804
|
+
similarity: similarity(referenceWord, attemptWord),
|
|
1805
|
+
status: referenceWord === attemptWord ? "correct" : "incorrect"
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
return words;
|
|
1809
|
+
}
|
|
495
1810
|
|
|
496
1811
|
// src/scoring/text-match.ts
|
|
497
1812
|
var COMBINING_MARKS_RE = /\p{M}/gu;
|
|
498
|
-
var
|
|
1813
|
+
var PUNCTUATION_RE2 = /\p{P}/gu;
|
|
499
1814
|
function applyBaseline(value, policy) {
|
|
500
1815
|
let result = value;
|
|
501
1816
|
if (policy.trim !== false) {
|
|
@@ -512,7 +1827,7 @@ function applyNormalization(value, policy) {
|
|
|
512
1827
|
result = result.normalize(policy.normalize);
|
|
513
1828
|
}
|
|
514
1829
|
if (policy.ignorePunctuation === true) {
|
|
515
|
-
result = result.replace(
|
|
1830
|
+
result = result.replace(PUNCTUATION_RE2, "");
|
|
516
1831
|
}
|
|
517
1832
|
if (policy.collapseInnerWhitespace === true) {
|
|
518
1833
|
result = result.replace(/(?<=\S)\s+(?=\S)/g, " ");
|
|
@@ -651,6 +1966,22 @@ var DRAFT_ISSUE_SEVERITY = {
|
|
|
651
1966
|
fib_placeholder_missing: "incomplete",
|
|
652
1967
|
fib_placeholder_duplicate: "invalid",
|
|
653
1968
|
fib_blanks_mismatch: "invalid",
|
|
1969
|
+
// item-group (the testlet container, via validateItemGroupDraft)
|
|
1970
|
+
ig_not_an_object: "invalid",
|
|
1971
|
+
ig_stimulus_required: "incomplete",
|
|
1972
|
+
ig_stimulus_id_required: "invalid",
|
|
1973
|
+
ig_stimulus_kind_required: "incomplete",
|
|
1974
|
+
ig_stimulus_kind_invalid: "invalid",
|
|
1975
|
+
ig_stimulus_body_required: "incomplete",
|
|
1976
|
+
ig_stimulus_media_required: "incomplete",
|
|
1977
|
+
ig_stimulus_media_kind: "invalid",
|
|
1978
|
+
ig_items_required: "incomplete",
|
|
1979
|
+
ig_item_id_required: "invalid",
|
|
1980
|
+
ig_item_id_duplicate: "invalid",
|
|
1981
|
+
ig_item_type_required: "incomplete",
|
|
1982
|
+
ig_item_type_unknown: "invalid",
|
|
1983
|
+
ig_item_nested_group: "invalid",
|
|
1984
|
+
ig_shuffle_invalid: "invalid",
|
|
654
1985
|
// gap-select
|
|
655
1986
|
gs_passage_required: "incomplete",
|
|
656
1987
|
gs_gaps_required: "incomplete",
|
|
@@ -684,7 +2015,29 @@ var DRAFT_ISSUE_SEVERITY = {
|
|
|
684
2015
|
wr_criterion_weight_required: "incomplete",
|
|
685
2016
|
wr_criterion_weight_invalid: "invalid",
|
|
686
2017
|
wr_rubric_weights_zero: "incomplete",
|
|
687
|
-
wr_rubric_weights_too_large: "invalid"
|
|
2018
|
+
wr_rubric_weights_too_large: "invalid",
|
|
2019
|
+
// dictation
|
|
2020
|
+
dc_transcript_required: "incomplete",
|
|
2021
|
+
dc_transcript_unscorable: "invalid",
|
|
2022
|
+
dc_transcript_too_long: "invalid",
|
|
2023
|
+
dc_accepted_transcript_empty: "incomplete",
|
|
2024
|
+
dc_accepted_transcript_too_long: "invalid",
|
|
2025
|
+
dc_accepted_transcript_duplicate: "invalid",
|
|
2026
|
+
dc_accepted_transcripts_too_many: "invalid",
|
|
2027
|
+
dc_media_kind: "invalid",
|
|
2028
|
+
dc_captions_not_allowed: "invalid",
|
|
2029
|
+
dc_slow_media_kind: "invalid",
|
|
2030
|
+
dc_slow_media_playback: "invalid",
|
|
2031
|
+
dc_slow_media_unbudgeted: "invalid",
|
|
2032
|
+
dc_slow_media_same_url: "invalid",
|
|
2033
|
+
dc_slow_media_without_media: "incomplete",
|
|
2034
|
+
dc_transcript_revealed: "invalid",
|
|
2035
|
+
dc_shown_text_too_long: "invalid",
|
|
2036
|
+
dc_hints_mode_required: "incomplete",
|
|
2037
|
+
dc_hints_mode_invalid: "invalid",
|
|
2038
|
+
dc_equivalence_from_required: "incomplete",
|
|
2039
|
+
dc_equivalence_to_required: "incomplete",
|
|
2040
|
+
dc_tolerance_invalid: "invalid"
|
|
688
2041
|
};
|
|
689
2042
|
function issue(code, path, message) {
|
|
690
2043
|
return { path: path.map(String), message, code, severity: DRAFT_ISSUE_SEVERITY[code] };
|
|
@@ -857,6 +2210,361 @@ function checkMedia(media, prefix = ["media"], schema = MediaSchema) {
|
|
|
857
2210
|
}
|
|
858
2211
|
return issues;
|
|
859
2212
|
}
|
|
2213
|
+
function covers(reported, failed) {
|
|
2214
|
+
if (failed.length === 0) {
|
|
2215
|
+
return reported.length === 0;
|
|
2216
|
+
}
|
|
2217
|
+
return failed.every((segment, index) => reported[index] === segment);
|
|
2218
|
+
}
|
|
2219
|
+
function pathKey(path) {
|
|
2220
|
+
return path.length === 0 ? "" : JSON.stringify(path.map(String));
|
|
2221
|
+
}
|
|
2222
|
+
function coveredPathsOf(reported) {
|
|
2223
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2224
|
+
for (const known of reported) {
|
|
2225
|
+
if (known.path.length === 0) {
|
|
2226
|
+
covered.add("");
|
|
2227
|
+
}
|
|
2228
|
+
for (let length = 1; length <= known.path.length; length += 1) {
|
|
2229
|
+
covered.add(pathKey(known.path.slice(0, length)));
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return covered;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/authoring/dictation.ts
|
|
2236
|
+
var dictationAuthoring = {
|
|
2237
|
+
createDraft: ({ newId }) => ({
|
|
2238
|
+
schemaVersion: "1.0",
|
|
2239
|
+
type: "dictation",
|
|
2240
|
+
id: newId(),
|
|
2241
|
+
title: "",
|
|
2242
|
+
transcript: ""
|
|
2243
|
+
}),
|
|
2244
|
+
checkDraft: checkDictationDraft
|
|
2245
|
+
};
|
|
2246
|
+
function checkDictationDraft(draft) {
|
|
2247
|
+
const issues = checkIdentity(draft, "dictation");
|
|
2248
|
+
const tolerance = isRecord(draft.tolerance) ? draft.tolerance : void 0;
|
|
2249
|
+
const normalizer = dictationNormalizer(tolerance);
|
|
2250
|
+
issues.push(...checkTranscript(draft.transcript, normalizer));
|
|
2251
|
+
issues.push(...checkAcceptedTranscripts(draft, normalizer));
|
|
2252
|
+
issues.push(...checkRecording(draft.media));
|
|
2253
|
+
issues.push(...checkSlowRecording(draft));
|
|
2254
|
+
issues.push(...checkRevealed(draft, normalizer));
|
|
2255
|
+
issues.push(...checkHints(draft.hints));
|
|
2256
|
+
issues.push(...checkTolerance(draft.tolerance));
|
|
2257
|
+
issues.push(...checkSharedOptional(draft));
|
|
2258
|
+
return issues;
|
|
2259
|
+
}
|
|
2260
|
+
function isUnwrittenTranscript(value) {
|
|
2261
|
+
if (typeof value === "string" && isLongerThan(value, DICTATION_MAX_TRANSCRIPT_LENGTH)) {
|
|
2262
|
+
return false;
|
|
2263
|
+
}
|
|
2264
|
+
return isUnwritten(value) || typeof value === "string" && isBlankBeforeRules(value);
|
|
2265
|
+
}
|
|
2266
|
+
function acceptedTranscriptsRead(accepted) {
|
|
2267
|
+
return accepted.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS);
|
|
2268
|
+
}
|
|
2269
|
+
function checkTranscript(transcript, normalizer) {
|
|
2270
|
+
if (isUnwrittenTranscript(transcript)) {
|
|
2271
|
+
return [issue("dc_transcript_required", ["transcript"], "Write the transcript.")];
|
|
2272
|
+
}
|
|
2273
|
+
if (typeof transcript !== "string") {
|
|
2274
|
+
return [];
|
|
2275
|
+
}
|
|
2276
|
+
const measured = normalizer.measure(transcript);
|
|
2277
|
+
if (measured.tooLong) {
|
|
2278
|
+
return [
|
|
2279
|
+
issue(
|
|
2280
|
+
"dc_transcript_too_long",
|
|
2281
|
+
["transcript"],
|
|
2282
|
+
`The transcript is longer than ${DICTATION_MAX_TRANSCRIPT_LENGTH} characters before or after its equivalences are applied, or they grow it past ${WORKING_LENGTH} characters on the way. A dictation is a sentence or two.`
|
|
2283
|
+
)
|
|
2284
|
+
];
|
|
2285
|
+
}
|
|
2286
|
+
if (measured.normalized === "") {
|
|
2287
|
+
return [
|
|
2288
|
+
issue(
|
|
2289
|
+
"dc_transcript_unscorable",
|
|
2290
|
+
["transcript"],
|
|
2291
|
+
"The transcript has nothing to type: once case, punctuation and spacing are ignored, nothing is left."
|
|
2292
|
+
)
|
|
2293
|
+
];
|
|
2294
|
+
}
|
|
2295
|
+
return [];
|
|
2296
|
+
}
|
|
2297
|
+
function checkAcceptedTranscripts(draft, normalizer) {
|
|
2298
|
+
const accepted = draft.acceptedTranscripts;
|
|
2299
|
+
if (!Array.isArray(accepted)) {
|
|
2300
|
+
return [];
|
|
2301
|
+
}
|
|
2302
|
+
const issues = [];
|
|
2303
|
+
if (accepted.length > DICTATION_MAX_ACCEPTED_TRANSCRIPTS) {
|
|
2304
|
+
issues.push(
|
|
2305
|
+
issue(
|
|
2306
|
+
"dc_accepted_transcripts_too_many",
|
|
2307
|
+
["acceptedTranscripts"],
|
|
2308
|
+
`At most ${DICTATION_MAX_ACCEPTED_TRANSCRIPTS} accepted transcripts. Prefer an equivalence for a word that varies.`
|
|
2309
|
+
)
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
for (const [index, text] of accepted.entries()) {
|
|
2313
|
+
if (typeof text !== "string") {
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
if (index >= DICTATION_MAX_ACCEPTED_TRANSCRIPTS) {
|
|
2317
|
+
if (text === "") {
|
|
2318
|
+
issues.push(acceptedTranscriptEmpty(index));
|
|
2319
|
+
}
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
const measured = normalizer.measure(text);
|
|
2323
|
+
if (measured.tooLong) {
|
|
2324
|
+
issues.push(
|
|
2325
|
+
issue(
|
|
2326
|
+
"dc_accepted_transcript_too_long",
|
|
2327
|
+
["acceptedTranscripts", index],
|
|
2328
|
+
`Accepted transcript ${index + 1} is longer than ${DICTATION_MAX_TRANSCRIPT_LENGTH} characters before or after its equivalences are applied, or they grow it past ${WORKING_LENGTH} characters on the way.`
|
|
2329
|
+
)
|
|
2330
|
+
);
|
|
2331
|
+
} else if (measured.normalized === "") {
|
|
2332
|
+
issues.push(acceptedTranscriptEmpty(index));
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
const candidates = [draft.transcript, ...acceptedTranscriptsRead(accepted)].filter((text) => typeof text === "string").map((text) => normalizer.measure(text).normalized).filter((text) => text !== null && text !== "");
|
|
2336
|
+
if (new Set(candidates).size !== candidates.length) {
|
|
2337
|
+
issues.push(
|
|
2338
|
+
issue(
|
|
2339
|
+
"dc_accepted_transcript_duplicate",
|
|
2340
|
+
["acceptedTranscripts"],
|
|
2341
|
+
"An accepted transcript repeats the transcript, or another accepted transcript, once case, punctuation and spacing are ignored. Remove it."
|
|
2342
|
+
)
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
return issues;
|
|
2346
|
+
}
|
|
2347
|
+
function acceptedTranscriptEmpty(index) {
|
|
2348
|
+
return issue(
|
|
2349
|
+
"dc_accepted_transcript_empty",
|
|
2350
|
+
["acceptedTranscripts", index],
|
|
2351
|
+
`Accepted transcript ${index + 1} has nothing to type. Fill it in or remove it.`
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
function checkRecording(media) {
|
|
2355
|
+
if (!isRecord(media)) {
|
|
2356
|
+
return [];
|
|
2357
|
+
}
|
|
2358
|
+
const issues = [];
|
|
2359
|
+
const kind = media.type;
|
|
2360
|
+
if (!isUnwritten(kind) && kind !== "audio") {
|
|
2361
|
+
issues.push(
|
|
2362
|
+
issue(
|
|
2363
|
+
"dc_media_kind",
|
|
2364
|
+
["media", "type"],
|
|
2365
|
+
"A dictation is dictated: its recording must be audio."
|
|
2366
|
+
)
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
if (!isUnset(media.captionsUrl)) {
|
|
2370
|
+
issues.push(
|
|
2371
|
+
issue("dc_captions_not_allowed", ["media", "captionsUrl"], CAPTIONS_ARE_THE_ANSWER)
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
return issues;
|
|
2375
|
+
}
|
|
2376
|
+
var CAPTIONS_ARE_THE_ANSWER = "A dictation recording cannot carry captions: the captions are the answer. Remove them, and offer an accessible alternative as a different item.";
|
|
2377
|
+
function checkSlowRecording(draft) {
|
|
2378
|
+
const slow = draft.slowMedia;
|
|
2379
|
+
if (!isRecord(slow)) {
|
|
2380
|
+
return [];
|
|
2381
|
+
}
|
|
2382
|
+
const issues = [];
|
|
2383
|
+
const kind = slow.type;
|
|
2384
|
+
const wrongKind = !isUnwritten(kind) && kind !== "audio";
|
|
2385
|
+
if (wrongKind) {
|
|
2386
|
+
issues.push(
|
|
2387
|
+
issue("dc_slow_media_kind", ["slowMedia", "type"], "The slow recording must be audio.")
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
issues.push(
|
|
2391
|
+
...checkMedia(
|
|
2392
|
+
wrongKind ? { ...slow, type: "audio" } : slow,
|
|
2393
|
+
["slowMedia"],
|
|
2394
|
+
DictationSlowMediaSchema
|
|
2395
|
+
)
|
|
2396
|
+
);
|
|
2397
|
+
if (!isUnset(slow.captionsUrl)) {
|
|
2398
|
+
issues.push(
|
|
2399
|
+
issue("dc_captions_not_allowed", ["slowMedia", "captionsUrl"], CAPTIONS_ARE_THE_ANSWER)
|
|
2400
|
+
);
|
|
2401
|
+
}
|
|
2402
|
+
if (slow.playback !== void 0) {
|
|
2403
|
+
issues.push(
|
|
2404
|
+
issue(
|
|
2405
|
+
"dc_slow_media_playback",
|
|
2406
|
+
["slowMedia", "playback"],
|
|
2407
|
+
"The slow recording has no playback policy of its own. Put the policy on the recording; the slow recording follows it, except the play budget."
|
|
2408
|
+
)
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
const media = draft.media;
|
|
2412
|
+
if (isUnset(media)) {
|
|
2413
|
+
issues.push(
|
|
2414
|
+
issue(
|
|
2415
|
+
"dc_slow_media_without_media",
|
|
2416
|
+
["media"],
|
|
2417
|
+
"A slow recording accompanies a recording. Add the recording first."
|
|
2418
|
+
)
|
|
2419
|
+
);
|
|
2420
|
+
return issues;
|
|
2421
|
+
}
|
|
2422
|
+
if (!isRecord(media)) {
|
|
2423
|
+
return issues;
|
|
2424
|
+
}
|
|
2425
|
+
if (isRecord(media.playback) && media.playback.maxPlays !== void 0) {
|
|
2426
|
+
issues.push(
|
|
2427
|
+
issue(
|
|
2428
|
+
"dc_slow_media_unbudgeted",
|
|
2429
|
+
["slowMedia"],
|
|
2430
|
+
"A play budget on the recording cannot coexist with a slow recording that has none. Remove the slow recording, or remove the play limit."
|
|
2431
|
+
)
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
if (typeof slow.url === "string" && slow.url !== "" && slow.url === media.url) {
|
|
2435
|
+
issues.push(
|
|
2436
|
+
issue(
|
|
2437
|
+
"dc_slow_media_same_url",
|
|
2438
|
+
["slowMedia", "url"],
|
|
2439
|
+
"The slow recording is the same file as the recording. Point it at the slower render."
|
|
2440
|
+
)
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
return issues;
|
|
2444
|
+
}
|
|
2445
|
+
function checkRevealed(draft, normalizer) {
|
|
2446
|
+
const candidates = [
|
|
2447
|
+
draft.transcript,
|
|
2448
|
+
...Array.isArray(draft.acceptedTranscripts) ? acceptedTranscriptsRead(draft.acceptedTranscripts) : []
|
|
2449
|
+
].filter((text) => typeof text === "string").map((text) => normalizer.measure(text));
|
|
2450
|
+
const shown = [
|
|
2451
|
+
[["title"], draft.title],
|
|
2452
|
+
[["media", "alt"], isRecord(draft.media) ? draft.media.alt : void 0],
|
|
2453
|
+
[["slowMedia", "alt"], isRecord(draft.slowMedia) ? draft.slowMedia.alt : void 0]
|
|
2454
|
+
];
|
|
2455
|
+
const issues = [];
|
|
2456
|
+
for (const [path, text] of shown) {
|
|
2457
|
+
if (typeof text !== "string") {
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
const { tooLong, forms } = normalizer.shown(text);
|
|
2461
|
+
if (tooLong) {
|
|
2462
|
+
issues.push(
|
|
2463
|
+
issue(
|
|
2464
|
+
"dc_shown_text_too_long",
|
|
2465
|
+
path,
|
|
2466
|
+
`${path[0] === "title" ? "The title" : "The description"} is too long to be checked for the transcript: at most ${WORKING_LENGTH} characters before and after the equivalences are applied, and they may not grow it past that on the way.`
|
|
2467
|
+
)
|
|
2468
|
+
);
|
|
2469
|
+
} else if (candidates.some((candidate) => revealsCandidate(forms, candidate))) {
|
|
2470
|
+
issues.push(
|
|
2471
|
+
issue(
|
|
2472
|
+
"dc_transcript_revealed",
|
|
2473
|
+
path,
|
|
2474
|
+
path[0] === "title" ? 'The title gives the answer away: it contains the transcript. Name the exercise instead ("Listen and type the sentence").' : 'The description gives the answer away: it contains the transcript. Describe the recording ("Recording, normal speed"), never transcribe it.'
|
|
2475
|
+
)
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return issues;
|
|
2480
|
+
}
|
|
2481
|
+
function checkHints(hints) {
|
|
2482
|
+
if (!isRecord(hints)) {
|
|
2483
|
+
return [];
|
|
2484
|
+
}
|
|
2485
|
+
const mode = hints.mode;
|
|
2486
|
+
if (isUnwritten(mode)) {
|
|
2487
|
+
return [issue("dc_hints_mode_required", ["hints", "mode"], "Choose how hints are given.")];
|
|
2488
|
+
}
|
|
2489
|
+
if (mode !== "progressive-words") {
|
|
2490
|
+
return [
|
|
2491
|
+
issue(
|
|
2492
|
+
"dc_hints_mode_invalid",
|
|
2493
|
+
["hints", "mode"],
|
|
2494
|
+
'The only hint mode is "progressive-words". Leave hints out unless you mean to offer them.'
|
|
2495
|
+
)
|
|
2496
|
+
];
|
|
2497
|
+
}
|
|
2498
|
+
return [];
|
|
2499
|
+
}
|
|
2500
|
+
function checkTolerance(tolerance) {
|
|
2501
|
+
if (isUnset(tolerance)) {
|
|
2502
|
+
return [];
|
|
2503
|
+
}
|
|
2504
|
+
if (!isRecord(tolerance)) {
|
|
2505
|
+
return [
|
|
2506
|
+
issue(
|
|
2507
|
+
"dc_tolerance_invalid",
|
|
2508
|
+
["tolerance"],
|
|
2509
|
+
"The tolerances are an object: `{ equivalences: [{ from, to }] }`."
|
|
2510
|
+
)
|
|
2511
|
+
];
|
|
2512
|
+
}
|
|
2513
|
+
const issues = [];
|
|
2514
|
+
const rules = tolerance.equivalences;
|
|
2515
|
+
if (Array.isArray(rules)) {
|
|
2516
|
+
if (rules.length > DICTATION_MAX_EQUIVALENCES) {
|
|
2517
|
+
issues.push(
|
|
2518
|
+
issue(
|
|
2519
|
+
"dc_tolerance_invalid",
|
|
2520
|
+
["tolerance", "equivalences"],
|
|
2521
|
+
`A dictation carries at most ${DICTATION_MAX_EQUIVALENCES} equivalence rules.`
|
|
2522
|
+
)
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
for (const [index, rule] of rules.entries()) {
|
|
2526
|
+
if (!isRecord(rule)) {
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
const path = ["tolerance", "equivalences", index];
|
|
2530
|
+
const from = rule.from;
|
|
2531
|
+
if (isUnwritten(from) || typeof from === "string" && preStripNormalize(from) === "") {
|
|
2532
|
+
issues.push(
|
|
2533
|
+
issue(
|
|
2534
|
+
"dc_equivalence_from_required",
|
|
2535
|
+
[...path, "from"],
|
|
2536
|
+
`Say what rule ${index + 1} rewrites: a word, a phrase or a symbol.`
|
|
2537
|
+
)
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
const to = rule.to;
|
|
2541
|
+
if (isUnwritten(to) || typeof to === "string" && normalizeDictationText(to) === "") {
|
|
2542
|
+
issues.push(
|
|
2543
|
+
issue(
|
|
2544
|
+
"dc_equivalence_to_required",
|
|
2545
|
+
[...path, "to"],
|
|
2546
|
+
`Say what rule ${index + 1} rewrites to. A rule cannot delete a word or leave only punctuation.`
|
|
2547
|
+
)
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
const parsed = DictationToleranceSchema.safeParse(tolerance, { reportInput: true });
|
|
2553
|
+
if (!parsed.success) {
|
|
2554
|
+
const covered = coveredPathsOf(issues);
|
|
2555
|
+
for (const schemaIssue of parsed.error.issues) {
|
|
2556
|
+
const path = ["tolerance", ...schemaIssue.path.map(String)];
|
|
2557
|
+
if (refusesEmpty(tolerance, schemaIssue) || covered.has(pathKey(path))) {
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
issues.push(issue("dc_tolerance_invalid", path, schemaIssue.message));
|
|
2561
|
+
for (const key of coveredPathsOf([{ path }])) {
|
|
2562
|
+
covered.add(key);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
return issues;
|
|
2567
|
+
}
|
|
860
2568
|
|
|
861
2569
|
// src/authoring/fill-in-the-blanks.ts
|
|
862
2570
|
var fillInTheBlanksAuthoring = {
|
|
@@ -1611,6 +3319,27 @@ function checkCriteria(criteria) {
|
|
|
1611
3319
|
return issues;
|
|
1612
3320
|
}
|
|
1613
3321
|
|
|
3322
|
+
// src/scoring/activity-scorers/dictation.ts
|
|
3323
|
+
function scoreDictation(data, response) {
|
|
3324
|
+
const alignment = alignDictation(data, typeof response?.text === "string" ? response.text : "");
|
|
3325
|
+
const details = [];
|
|
3326
|
+
for (const word of alignment.words) {
|
|
3327
|
+
if (word.status === "extra") {
|
|
3328
|
+
continue;
|
|
3329
|
+
}
|
|
3330
|
+
details.push({
|
|
3331
|
+
itemId: word.itemId,
|
|
3332
|
+
correct: word.status === "correct",
|
|
3333
|
+
outcome: word.status === "correct" ? "correct" : word.status === "incorrect" ? "incorrect" : "incorrect-omission",
|
|
3334
|
+
learnerResponse: word.attempt,
|
|
3335
|
+
correctResponse: word.reference,
|
|
3336
|
+
score: word.similarity,
|
|
3337
|
+
weight: 1
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
return { score: alignment.similarity, maxScore: 1, feedback: null, details };
|
|
3341
|
+
}
|
|
3342
|
+
|
|
1614
3343
|
// src/scoring/strategies/all-or-nothing.ts
|
|
1615
3344
|
function allOrNothingStrategy(correctItems) {
|
|
1616
3345
|
return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;
|
|
@@ -1838,6 +3567,15 @@ var FILL_IN_THE_BLANKS_FIELD_POLICY = {
|
|
|
1838
3567
|
feedback: "answer-key"
|
|
1839
3568
|
}
|
|
1840
3569
|
};
|
|
3570
|
+
var DICTATION_FIELD_POLICY = {
|
|
3571
|
+
...SHARED_PUBLIC_FIELDS,
|
|
3572
|
+
transcript: "answer-key",
|
|
3573
|
+
acceptedTranscripts: "answer-key",
|
|
3574
|
+
slowMedia: { type: "public", url: "public", alt: "public" },
|
|
3575
|
+
hints: { mode: "public" },
|
|
3576
|
+
tolerance: "answer-key",
|
|
3577
|
+
feedback: FEEDBACK_FIELD_POLICY
|
|
3578
|
+
};
|
|
1841
3579
|
var RUBRIC_FIELD_POLICY = {
|
|
1842
3580
|
label: "public",
|
|
1843
3581
|
// Applies to every element of the array.
|
|
@@ -1951,29 +3689,77 @@ var gapSelectType = defineActivityType({
|
|
|
1951
3689
|
interactions: ["gap-selected", "submitted"],
|
|
1952
3690
|
authoring: gapSelectAuthoring
|
|
1953
3691
|
});
|
|
3692
|
+
var dictationType = defineActivityType({
|
|
3693
|
+
type: "dictation",
|
|
3694
|
+
schema: DictationDataSchema,
|
|
3695
|
+
scoring: { kind: "sync", score: scoreDictation },
|
|
3696
|
+
// Whitespace-only or punctuation-only text is no answer. Without the
|
|
3697
|
+
// tolerance, which this signature cannot see: the one gap is an attempt made
|
|
3698
|
+
// ONLY of a symbol an equivalence names, which reads as unanswered here and
|
|
3699
|
+
// is scored as the rewritten word — documented in the authoring guide. The
|
|
3700
|
+
// reverse cannot happen: a rule inserts words, never nothing, and the text is
|
|
3701
|
+
// cut where the scorer cuts, so text past the cap never counts as an answer.
|
|
3702
|
+
isAnswered: (response) => normalizeDictationText(
|
|
3703
|
+
truncateCodePoints(
|
|
3704
|
+
typeof response?.text === "string" ? response.text : "",
|
|
3705
|
+
DICTATION_MAX_TEXT_LENGTH
|
|
3706
|
+
)
|
|
3707
|
+
) !== "",
|
|
3708
|
+
fieldPolicy: DICTATION_FIELD_POLICY,
|
|
3709
|
+
redactedSchema: RedactedDictationDataSchema,
|
|
3710
|
+
interop: {
|
|
3711
|
+
xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
|
3712
|
+
// `fill-in`, as for fill-in-the-blanks: a typed answer with a key pattern.
|
|
3713
|
+
// `long-fill-in` is what written-response uses for free text nothing scores.
|
|
3714
|
+
xapiInteractionType: "fill-in",
|
|
3715
|
+
correctResponsesPattern: (data) => [data.transcript, ...data.acceptedTranscripts ?? []]
|
|
3716
|
+
},
|
|
3717
|
+
interactions: ["text-changed", "hint-requested", "submitted"],
|
|
3718
|
+
authoring: dictationAuthoring
|
|
3719
|
+
});
|
|
1954
3720
|
registerActivityType(multipleChoiceType);
|
|
1955
3721
|
registerActivityType(fillInTheBlanksType);
|
|
1956
3722
|
registerActivityType(writtenResponseType);
|
|
1957
3723
|
registerActivityType(gapSelectType);
|
|
3724
|
+
registerActivityType(dictationType);
|
|
1958
3725
|
|
|
1959
3726
|
export {
|
|
1960
3727
|
ActivitySchemaError,
|
|
1961
3728
|
UnknownActivityTypeError,
|
|
1962
3729
|
RedactedScoringError,
|
|
1963
3730
|
DeferredScoringError,
|
|
3731
|
+
withValidationScope,
|
|
3732
|
+
DICTATION_MAX_TRANSCRIPT_LENGTH,
|
|
3733
|
+
DICTATION_MAX_TEXT_LENGTH,
|
|
3734
|
+
DICTATION_MAX_EQUIVALENCE_LENGTH,
|
|
3735
|
+
DICTATION_MAX_EQUIVALENCES,
|
|
3736
|
+
DICTATION_MAX_ACCEPTED_TRANSCRIPTS,
|
|
1964
3737
|
FeedbackSchema,
|
|
1965
3738
|
MediaUrlSchema,
|
|
1966
3739
|
NativeControlHintSchema,
|
|
1967
3740
|
MediaPlaybackSchema,
|
|
1968
3741
|
MediaSchema,
|
|
1969
3742
|
RedactedMediaSchema,
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
3743
|
+
DictationSlowMediaSchema,
|
|
3744
|
+
DictationEquivalenceSchema,
|
|
3745
|
+
DictationToleranceSchema,
|
|
3746
|
+
DictationDataSchema,
|
|
3747
|
+
groupCaptionsRevealDictation,
|
|
3748
|
+
GROUP_CAPTIONS_REVEAL_DICTATION,
|
|
1973
3749
|
DRAFT_ISSUE_SEVERITY,
|
|
1974
3750
|
issue,
|
|
1975
3751
|
isRecord,
|
|
1976
3752
|
refusesEmpty,
|
|
3753
|
+
isUnset,
|
|
3754
|
+
isUnwritten,
|
|
3755
|
+
isMissingId,
|
|
3756
|
+
checkMedia,
|
|
3757
|
+
covers,
|
|
3758
|
+
pathKey,
|
|
3759
|
+
coveredPathsOf,
|
|
3760
|
+
TextMatchPolicySchema,
|
|
3761
|
+
BlankConfigSchema,
|
|
3762
|
+
FillInTheBlanksDataSchema,
|
|
1977
3763
|
MultipleChoiceOptionMediaSchema,
|
|
1978
3764
|
MultipleChoiceOptionSchema,
|
|
1979
3765
|
MultipleChoiceDataSchema,
|
|
@@ -1995,6 +3781,12 @@ export {
|
|
|
1995
3781
|
RedactedGapSelectGapSchema,
|
|
1996
3782
|
RedactedGapSelectDataSchema,
|
|
1997
3783
|
RedactedWrittenResponseDataSchema,
|
|
3784
|
+
RedactedDictationSlowMediaSchema,
|
|
3785
|
+
RedactedDictationDataSchema,
|
|
3786
|
+
dictationReferenceWords,
|
|
3787
|
+
alignDictation,
|
|
3788
|
+
STEP,
|
|
3789
|
+
alignSequences,
|
|
1998
3790
|
levenshteinDistance,
|
|
1999
3791
|
matchText,
|
|
2000
3792
|
defineActivityType,
|
|
@@ -2004,6 +3796,8 @@ export {
|
|
|
2004
3796
|
MEDIA_FIELD_POLICY,
|
|
2005
3797
|
multipleChoiceType,
|
|
2006
3798
|
fillInTheBlanksType,
|
|
2007
|
-
writtenResponseType
|
|
3799
|
+
writtenResponseType,
|
|
3800
|
+
gapSelectType,
|
|
3801
|
+
dictationType
|
|
2008
3802
|
};
|
|
2009
|
-
//# sourceMappingURL=chunk-
|
|
3803
|
+
//# sourceMappingURL=chunk-XKKKWKRH.js.map
|