@intellectif/lk-core 1.4.0 → 1.6.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 +102 -0
- package/README.md +2 -0
- package/dist/ai-check.cjs +175 -17
- package/dist/ai-check.cjs.map +1 -1
- package/dist/ai-check.d.cts +13 -5
- package/dist/ai-check.d.cts.map +1 -1
- package/dist/ai-check.d.ts +13 -5
- package/dist/ai-check.d.ts.map +1 -1
- package/dist/ai-check.js +175 -17
- package/dist/ai-check.js.map +1 -1
- package/dist/{ai-CnmLPMzd.d.ts → ai-i3jwXnpX.d.cts} +242 -7
- package/dist/ai-i3jwXnpX.d.cts.map +1 -0
- package/dist/{ai-CnmLPMzd.d.cts → ai-i3jwXnpX.d.ts} +242 -7
- package/dist/ai-i3jwXnpX.d.ts.map +1 -0
- package/dist/{ai-writing-G6tnCGzw.cjs → ai-writing-C4YRVrFi.cjs} +683 -72
- package/dist/ai-writing-C4YRVrFi.cjs.map +1 -0
- package/dist/{ai-writing-CPpH9Qxq.js → ai-writing-OJ5m_1EB.js} +663 -88
- package/dist/ai-writing-OJ5m_1EB.js.map +1 -0
- package/dist/{index-BqMXWuNC.d.cts → index--U7hHV0s.d.cts} +2 -2
- package/dist/{index-BqMXWuNC.d.cts.map → index--U7hHV0s.d.cts.map} +1 -1
- package/dist/{index-B54baFUA.d.ts → index-CFRgZLhH.d.cts} +45 -12
- package/dist/index-CFRgZLhH.d.cts.map +1 -0
- package/dist/{index-CVWvymVb.d.ts → index-DeD8EI1D.d.ts} +327 -3
- package/dist/index-DeD8EI1D.d.ts.map +1 -0
- package/dist/{index-CcHDTxIZ.d.ts → index-Dj92T76o.d.ts} +2 -2
- package/dist/{index-CcHDTxIZ.d.ts.map → index-Dj92T76o.d.ts.map} +1 -1
- package/dist/{index-BAfEHCnD.d.cts → index-Uh1u57yq.d.cts} +327 -3
- package/dist/index-Uh1u57yq.d.cts.map +1 -0
- package/dist/{index-2nigSf4N.d.cts → index-WBHSrIUU.d.ts} +45 -12
- package/dist/index-WBHSrIUU.d.ts.map +1 -0
- package/dist/index.cjs +88 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +217 -95
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +217 -95
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -118
- package/dist/index.js.map +1 -1
- package/dist/{item-group-DXiR8nOr.js → item-group-BbvvH_Ob.js} +3 -2
- package/dist/item-group-BbvvH_Ob.js.map +1 -0
- package/dist/{item-group-DroFXgLb.cjs → item-group-CQRbb8Rx.cjs} +3 -2
- package/dist/item-group-CQRbb8Rx.cjs.map +1 -0
- package/dist/{registry-Dsx9mNsS.js → registry-B23o9UWh.js} +218 -21
- package/dist/registry-B23o9UWh.js.map +1 -0
- package/dist/{registry-DVi0LR_i.cjs → registry-Ih5n4PVZ.cjs} +265 -20
- package/dist/registry-Ih5n4PVZ.cjs.map +1 -0
- package/dist/{schemas-DVbAOrtd.cjs → schemas-Q2dyOgc2.cjs} +77 -14
- package/dist/schemas-Q2dyOgc2.cjs.map +1 -0
- package/dist/{schemas-Bdw0nGPo.js → schemas-yId-bJhD.js} +72 -15
- package/dist/schemas-yId-bJhD.js.map +1 -0
- package/dist/schemas.cjs +5 -2
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +4 -3
- package/dist/{scoring-BZqUkuDF.js → scoring-CLMdk8i9.js} +657 -3
- package/dist/scoring-CLMdk8i9.js.map +1 -0
- package/dist/{scoring-B2_HEhMT.cjs → scoring-CaPS6q8b.cjs} +722 -2
- package/dist/scoring-CaPS6q8b.cjs.map +1 -0
- package/dist/scoring.cjs +8 -2
- package/dist/scoring.d.cts +3 -3
- package/dist/scoring.d.ts +3 -3
- package/dist/scoring.js +3 -3
- package/dist/{xapi-DNzkSJ0S.cjs → xapi-BX0hvRRB.cjs} +2 -2
- package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-BX0hvRRB.cjs.map} +1 -1
- package/dist/{xapi-CMTph7wG.js → xapi-Cdk9fTvV.js} +2 -2
- package/dist/{xapi-CMTph7wG.js.map → xapi-Cdk9fTvV.js.map} +1 -1
- package/dist/xapi.cjs +1 -1
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +1 -2
- package/package.json +2 -2
- package/vectors/README.md +24 -1
- package/vectors/scoring.json +18182 -1
- package/dist/ai-CnmLPMzd.d.cts.map +0 -1
- package/dist/ai-CnmLPMzd.d.ts.map +0 -1
- package/dist/ai-writing-CPpH9Qxq.js.map +0 -1
- package/dist/ai-writing-G6tnCGzw.cjs.map +0 -1
- package/dist/index-2nigSf4N.d.cts.map +0 -1
- package/dist/index-B54baFUA.d.ts.map +0 -1
- package/dist/index-BAfEHCnD.d.cts.map +0 -1
- package/dist/index-CVWvymVb.d.ts.map +0 -1
- package/dist/item-group-DXiR8nOr.js.map +0 -1
- package/dist/item-group-DroFXgLb.cjs.map +0 -1
- package/dist/registry-DVi0LR_i.cjs.map +0 -1
- package/dist/registry-Dsx9mNsS.js.map +0 -1
- package/dist/schemas-Bdw0nGPo.js.map +0 -1
- package/dist/schemas-DVbAOrtd.cjs.map +0 -1
- package/dist/scoring-B2_HEhMT.cjs.map +0 -1
- package/dist/scoring-BZqUkuDF.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const require_registry = require("./registry-
|
|
2
|
-
const require_scoring = require("./scoring-
|
|
3
|
-
const require_item_group = require("./item-group-
|
|
1
|
+
const require_registry = require("./registry-Ih5n4PVZ.cjs");
|
|
2
|
+
const require_scoring = require("./scoring-CaPS6q8b.cjs");
|
|
3
|
+
const require_item_group = require("./item-group-CQRbb8Rx.cjs");
|
|
4
4
|
//#region src/ai-material.ts
|
|
5
5
|
/**
|
|
6
6
|
* What a model is told about the group a question sits in: the material the
|
|
@@ -28,7 +28,7 @@ const MEDIA_TYPES = {
|
|
|
28
28
|
video: true,
|
|
29
29
|
embed: true
|
|
30
30
|
};
|
|
31
|
-
const isRecord$
|
|
31
|
+
const isRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
32
|
/** A string with something in it, as written; otherwise nothing. */
|
|
33
33
|
const filled = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
34
34
|
/**
|
|
@@ -62,17 +62,17 @@ function cut(text) {
|
|
|
62
62
|
* and a signed address must not end up in a prompt. Each field is read once.
|
|
63
63
|
*/
|
|
64
64
|
function materialFacts(group) {
|
|
65
|
-
if (!isRecord$
|
|
65
|
+
if (!isRecord$5(group)) return null;
|
|
66
66
|
const stimulus = group.stimulus;
|
|
67
|
-
if (!isRecord$
|
|
67
|
+
if (!isRecord$5(stimulus)) return null;
|
|
68
68
|
const kind = stimulus.kind;
|
|
69
69
|
if (typeof kind !== "string" || !Object.hasOwn(KINDS$2, kind)) return null;
|
|
70
70
|
const title = filled(stimulus.title) ?? filled(group.title);
|
|
71
71
|
const body = filled(stimulus.body);
|
|
72
72
|
const text = body === void 0 ? void 0 : cut(body);
|
|
73
73
|
const rawMedia = stimulus.media;
|
|
74
|
-
const mediaType = isRecord$
|
|
75
|
-
const alt = isRecord$
|
|
74
|
+
const mediaType = isRecord$5(rawMedia) ? rawMedia.type : void 0;
|
|
75
|
+
const alt = isRecord$5(rawMedia) ? filled(rawMedia.alt) : void 0;
|
|
76
76
|
const media = typeof mediaType === "string" && Object.hasOwn(MEDIA_TYPES, mediaType) ? {
|
|
77
77
|
type: mediaType,
|
|
78
78
|
...alt !== void 0 ? { alt } : {}
|
|
@@ -742,7 +742,7 @@ function aiCoachingRequest(input) {
|
|
|
742
742
|
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
743
743
|
};
|
|
744
744
|
}
|
|
745
|
-
const isRecord$
|
|
745
|
+
const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
746
746
|
/** A symbol as it is compared: composed, and without the spaces around it. */
|
|
747
747
|
const symbolKey = (symbol) => symbol.normalize("NFC").trim();
|
|
748
748
|
/**
|
|
@@ -791,7 +791,7 @@ function checkAiCoaching(raw, request) {
|
|
|
791
791
|
});
|
|
792
792
|
const seen = /* @__PURE__ */ new Set();
|
|
793
793
|
for (const entry of entries) {
|
|
794
|
-
if (!isRecord$
|
|
794
|
+
if (!isRecord$4(entry) || typeof entry.itemId !== "string" || typeof entry.tip !== "string") return {
|
|
795
795
|
ok: false,
|
|
796
796
|
refusal: "malformed"
|
|
797
797
|
};
|
|
@@ -839,7 +839,7 @@ function checkAiCoaching(raw, request) {
|
|
|
839
839
|
/** A sound a model names, checked against the sounds the engine reported for the word. */
|
|
840
840
|
function checkSound(raw, fact) {
|
|
841
841
|
if (raw === void 0 || raw === null) return;
|
|
842
|
-
if (!isRecord$
|
|
842
|
+
if (!isRecord$4(raw) || typeof raw.expected !== "string" || raw.heard !== void 0 && raw.heard !== null && typeof raw.heard !== "string") return {
|
|
843
843
|
ok: false,
|
|
844
844
|
refusal: "malformed"
|
|
845
845
|
};
|
|
@@ -866,6 +866,240 @@ function checkSound(raw, fact) {
|
|
|
866
866
|
};
|
|
867
867
|
}
|
|
868
868
|
//#endregion
|
|
869
|
+
//#region src/media-refs.ts
|
|
870
|
+
const MAIN_KINDS = [
|
|
871
|
+
"audio",
|
|
872
|
+
"video",
|
|
873
|
+
"image",
|
|
874
|
+
"embed"
|
|
875
|
+
];
|
|
876
|
+
function isRecord$3(value) {
|
|
877
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
878
|
+
}
|
|
879
|
+
function valueAt(root, path) {
|
|
880
|
+
let at = root;
|
|
881
|
+
for (const key of path) {
|
|
882
|
+
if (typeof key === "number" ? !Array.isArray(at) : !isRecord$3(at)) return;
|
|
883
|
+
at = at[key];
|
|
884
|
+
}
|
|
885
|
+
return at;
|
|
886
|
+
}
|
|
887
|
+
/** Where `item` keeps its files: what its type declares, or its `media` alone. */
|
|
888
|
+
function slotsOf(item) {
|
|
889
|
+
const declared = typeof item.type === "string" ? require_registry.getActivityTypeDescriptor(item.type)?.mediaSlots : void 0;
|
|
890
|
+
if (declared === void 0) return [{ path: ["media"] }];
|
|
891
|
+
const slots = declared(item);
|
|
892
|
+
return Array.isArray(slots) ? slots : [];
|
|
893
|
+
}
|
|
894
|
+
function holderRefs(holder, path, kindOfSlot, owner) {
|
|
895
|
+
if (!isRecord$3(holder)) return [];
|
|
896
|
+
const main = typeof holder.type === "string" && MAIN_KINDS.includes(holder.type) ? holder.type : kindOfSlot;
|
|
897
|
+
const refs = [];
|
|
898
|
+
const add = (field, kind, url) => {
|
|
899
|
+
refs.push({
|
|
900
|
+
path: [...path, ...field],
|
|
901
|
+
kind,
|
|
902
|
+
url,
|
|
903
|
+
holder,
|
|
904
|
+
...owner
|
|
905
|
+
});
|
|
906
|
+
};
|
|
907
|
+
if (main !== void 0) add(["url"], main, typeof holder.url === "string" ? holder.url : null);
|
|
908
|
+
if (typeof holder.captionsUrl === "string") add(["captionsUrl"], "captions", holder.captionsUrl);
|
|
909
|
+
if (Array.isArray(holder.tracks)) holder.tracks.forEach((track, index) => {
|
|
910
|
+
if (isRecord$3(track) && typeof track.src === "string") refs.push({
|
|
911
|
+
path: [
|
|
912
|
+
...path,
|
|
913
|
+
"tracks",
|
|
914
|
+
index,
|
|
915
|
+
"src"
|
|
916
|
+
],
|
|
917
|
+
kind: "track",
|
|
918
|
+
url: track.src,
|
|
919
|
+
holder: track,
|
|
920
|
+
...owner,
|
|
921
|
+
...typeof track.srclang === "string" ? { srclang: track.srclang } : {}
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
if (typeof holder.poster === "string") add(["poster"], "poster", holder.poster);
|
|
925
|
+
return refs;
|
|
926
|
+
}
|
|
927
|
+
function activityRefs(item, base) {
|
|
928
|
+
if (!isRecord$3(item)) return [];
|
|
929
|
+
const owner = typeof item.id === "string" ? { activityId: item.id } : {};
|
|
930
|
+
return slotsOf(item).flatMap((slot) => holderRefs(valueAt(item, slot.path), [...base, ...slot.path], slot.kind, owner));
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Every media address in one piece of content: a loose activity or an item
|
|
934
|
+
* group (its stimulus first, then each item in authored order), as a
|
|
935
|
+
* `SequenceEntry` holds them. Drafts are read too.
|
|
936
|
+
*
|
|
937
|
+
* For each activity the walker reads the places its type keeps files: an
|
|
938
|
+
* item's `media`, each multiple-choice option's `media`, and dictation's and
|
|
939
|
+
* read-aloud's `slowMedia`. In each it lists the main file, `captionsUrl`,
|
|
940
|
+
* every track's `src` and `poster`. A type you register declares its own with
|
|
941
|
+
* `mediaSlots`; without that, its `media` is read.
|
|
942
|
+
*
|
|
943
|
+
* A main file with no address yet is listed with `url: null`, so a recording
|
|
944
|
+
* still to be made has a place in the list. Not read: addresses inside an
|
|
945
|
+
* `…Html` sidecar (the SDK does not parse HTML) and a learner's recordings,
|
|
946
|
+
* which are responses rather than content.
|
|
947
|
+
*
|
|
948
|
+
* ```ts
|
|
949
|
+
* const files = listMediaRefs(entry).filter((ref) => ref.url !== null);
|
|
950
|
+
* ```
|
|
951
|
+
*/
|
|
952
|
+
function listMediaRefs(entry) {
|
|
953
|
+
if (!isRecord$3(entry)) return [];
|
|
954
|
+
if (entry.type !== "item-group") return activityRefs(entry, []);
|
|
955
|
+
const refs = [];
|
|
956
|
+
const stimulus = entry.stimulus;
|
|
957
|
+
if (isRecord$3(stimulus)) {
|
|
958
|
+
const owner = typeof stimulus.id === "string" ? { stimulusId: stimulus.id } : {};
|
|
959
|
+
refs.push(...holderRefs(stimulus.media, ["stimulus", "media"], void 0, owner));
|
|
960
|
+
}
|
|
961
|
+
if (Array.isArray(entry.items)) entry.items.forEach((item, index) => {
|
|
962
|
+
refs.push(...activityRefs(item, ["items", index]));
|
|
963
|
+
});
|
|
964
|
+
return refs;
|
|
965
|
+
}
|
|
966
|
+
/** A deep copy of a JSON value. Content is plain data; a cycle cannot be copied. */
|
|
967
|
+
function copyOf(value, seen) {
|
|
968
|
+
if (typeof value !== "object" || value === null) return value;
|
|
969
|
+
if (seen.has(value)) throw new TypeError("mapMediaRefs: the content contains a circular reference.");
|
|
970
|
+
seen.add(value);
|
|
971
|
+
try {
|
|
972
|
+
if (Array.isArray(value)) return value.map((element) => copyOf(element, seen));
|
|
973
|
+
const copy = {};
|
|
974
|
+
for (const [key, field] of Object.entries(value)) copy[key] = copyOf(field, seen);
|
|
975
|
+
return copy;
|
|
976
|
+
} finally {
|
|
977
|
+
seen.delete(value);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* A copy of `entry` with each address {@link listMediaRefs} finds replaced
|
|
982
|
+
* by what `fn` returns for it: files moved into a new folder structure, or an
|
|
983
|
+
* export's file names pointed at where an import stored them. Nothing else
|
|
984
|
+
* changes, and `entry` is never modified: with `(ref) => ref.url`, the copy is
|
|
985
|
+
* deep-equal to it, with the same `canonicalJson`.
|
|
986
|
+
*
|
|
987
|
+
* For a main file with no address yet (`url: null`), a string attaches one
|
|
988
|
+
* and `null` leaves it empty. For an address that is there, `fn` must return
|
|
989
|
+
* a string: removing a file is not mapping.
|
|
990
|
+
*
|
|
991
|
+
* The copy validates as the original did when every address `fn` returns is
|
|
992
|
+
* one the media rule accepts: `https:`, `http:`, `data:`, `blob:`, or
|
|
993
|
+
* root-relative (`/…`). A bundle-relative `media/part1.mp3` is refused.
|
|
994
|
+
*
|
|
995
|
+
* @throws TypeError when `fn` returns something other than a string or
|
|
996
|
+
* `null`, or `null` for an address that is there, or when `entry` is circular.
|
|
997
|
+
*/
|
|
998
|
+
function mapMediaRefs(entry, fn) {
|
|
999
|
+
const refs = listMediaRefs(entry);
|
|
1000
|
+
const copy = copyOf(entry, /* @__PURE__ */ new Set());
|
|
1001
|
+
for (const ref of refs) {
|
|
1002
|
+
const next = fn(ref);
|
|
1003
|
+
if (next === null) {
|
|
1004
|
+
if (ref.url !== null) throw new TypeError(`mapMediaRefs: ${ref.path.join(".")} has a file; returning null would remove it, which is not mapping.`);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (typeof next !== "string") throw new TypeError(`mapMediaRefs: the function must return a string or null for ${ref.path.join(".")}, not ${typeof next}.`);
|
|
1008
|
+
if (next === ref.url) continue;
|
|
1009
|
+
const holder = valueAt(copy, ref.path.slice(0, -1));
|
|
1010
|
+
holder[ref.path[ref.path.length - 1]] = next;
|
|
1011
|
+
}
|
|
1012
|
+
return copy;
|
|
1013
|
+
}
|
|
1014
|
+
/** A holder's own text: its transcript, else its script as "Speaker: text" lines. */
|
|
1015
|
+
function ownText(holder) {
|
|
1016
|
+
if (typeof holder.transcript === "string" && holder.transcript.trim() !== "") return holder.transcript;
|
|
1017
|
+
const turns = Array.isArray(holder.script) ? holder.script.flatMap((turn) => isRecord$3(turn) && typeof turn.speaker === "string" && typeof turn.text === "string" && turn.text.trim() !== "" ? [`${turn.speaker.trim()}: ${turn.text.trim()}`] : []) : [];
|
|
1018
|
+
return turns.length > 0 ? turns.join("\n") : null;
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* The author-side text of a recording: its `transcript`, or else its
|
|
1022
|
+
* `script` as one "Speaker: text" line per turn. `null` when it has neither.
|
|
1023
|
+
*
|
|
1024
|
+
* `holder` is a media object, a stimulus, or an activity. A stimulus or an
|
|
1025
|
+
* activity is read for its own text first and then for its `media`'s; a
|
|
1026
|
+
* stimulus keeps both at its own level. Author-only: pass it to the author's
|
|
1027
|
+
* requests (`aiDraftsRequest`'s `source`, a critique), never to a learner's.
|
|
1028
|
+
*
|
|
1029
|
+
* ```ts
|
|
1030
|
+
* const text = transcriptOf(item.media);
|
|
1031
|
+
* if (text !== null) aiDraftsRequest({ types: ['multiple-choice'], source: { kind: 'transcript', text } });
|
|
1032
|
+
* ```
|
|
1033
|
+
*/
|
|
1034
|
+
function transcriptOf(holder) {
|
|
1035
|
+
if (!isRecord$3(holder)) return null;
|
|
1036
|
+
return ownText(holder) ?? (isRecord$3(holder.media) ? ownText(holder.media) : null);
|
|
1037
|
+
}
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/authoring/recordings.ts
|
|
1040
|
+
const written$1 = (value) => typeof value === "string" && value.trim() !== "";
|
|
1041
|
+
/** The script's turns that have a speaker and words, or none. */
|
|
1042
|
+
function turnsOf(script) {
|
|
1043
|
+
return Array.isArray(script) ? script.flatMap((turn) => require_registry.isRecord(turn) && typeof turn.speaker === "string" && written$1(turn.text) ? [{
|
|
1044
|
+
speaker: turn.speaker,
|
|
1045
|
+
text: turn.text
|
|
1046
|
+
}] : []) : [];
|
|
1047
|
+
}
|
|
1048
|
+
const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1049
|
+
/** A transcript's words, with each `Speaker:` label of the script's speakers taken out. */
|
|
1050
|
+
function transcriptWords(transcript, speakers) {
|
|
1051
|
+
const labels = [...new Set(speakers.map((speaker) => speaker.trim()).filter(written$1))];
|
|
1052
|
+
const unlabelled = labels.length === 0 ? transcript : transcript.replace(new RegExp(`(^|\\s)(?:${labels.map(escaped).join("|")})\\s*:`, "giu"), "$1");
|
|
1053
|
+
return require_registry.fold(unlabelled);
|
|
1054
|
+
}
|
|
1055
|
+
/** What a recording says, as words: its transcript, or else its script's turns. */
|
|
1056
|
+
function spokenWords(holder) {
|
|
1057
|
+
const turns = turnsOf(holder.script);
|
|
1058
|
+
if (written$1(holder.transcript)) return transcriptWords(holder.transcript, turns.map((turn) => turn.speaker));
|
|
1059
|
+
return turns.length > 0 ? require_registry.fold(turns.map((turn) => turn.text).join(" ")) : null;
|
|
1060
|
+
}
|
|
1061
|
+
const sameWords = (a, b) => a.length === b.length && a.every((word, index) => word === b[index]);
|
|
1062
|
+
/** One object's own rules: text on media with nothing to say, and a transcript its script contradicts. */
|
|
1063
|
+
function holderFindings(holder, path) {
|
|
1064
|
+
if (!require_registry.isRecord(holder)) return [];
|
|
1065
|
+
const findings = [];
|
|
1066
|
+
const turns = turnsOf(holder.script);
|
|
1067
|
+
if ((written$1(holder.transcript) || turns.length > 0) && (holder.type === "image" || holder.type === "embed")) findings.push(require_registry.finding("media_transcript_unused", [...path, written$1(holder.transcript) ? "transcript" : "script"], `A transcript or a script on ${holder.type === "image" ? "a picture" : "an embedded player"} has nothing to describe, and nothing reads it. Put it on the recording it belongs to.`));
|
|
1068
|
+
if (written$1(holder.transcript) && turns.length > 0) {
|
|
1069
|
+
const fromTranscript = transcriptWords(holder.transcript, turns.map((turn) => turn.speaker));
|
|
1070
|
+
if (!sameWords(fromTranscript, require_registry.fold(turns.map((turn) => turn.text).join(" ")))) findings.push(require_registry.finding("media_script_differs", [...path, "script"], "The script and the transcript say different words. AI facts read the transcript, and a voice reads the script: make them say the same."));
|
|
1071
|
+
}
|
|
1072
|
+
return findings;
|
|
1073
|
+
}
|
|
1074
|
+
/** The field a type's own recording reads out, for the types that have one. */
|
|
1075
|
+
const SPOKEN_FIELD = {
|
|
1076
|
+
dictation: "transcript",
|
|
1077
|
+
"read-aloud": "referenceText"
|
|
1078
|
+
};
|
|
1079
|
+
/**
|
|
1080
|
+
* An item's recording rules, at every place its type keeps media. A
|
|
1081
|
+
* dictation's and a read-aloud's own text is what their recording says, so a
|
|
1082
|
+
* recording that says other words is reported too.
|
|
1083
|
+
*/
|
|
1084
|
+
function recordingFindings(item) {
|
|
1085
|
+
const findings = listMediaRefs(item).filter((ref) => ref.path[ref.path.length - 1] === "url").flatMap((ref) => holderFindings(ref.holder, ref.path.slice(0, -1)));
|
|
1086
|
+
const field = typeof item.type === "string" ? SPOKEN_FIELD[item.type] : void 0;
|
|
1087
|
+
const media = item.media;
|
|
1088
|
+
if (field === void 0 || !require_registry.isRecord(media)) return findings;
|
|
1089
|
+
const said = spokenWords(media);
|
|
1090
|
+
const accepted = [item[field], ...item.type === "dictation" && Array.isArray(item.acceptedTranscripts) ? item.acceptedTranscripts : []].filter(written$1);
|
|
1091
|
+
if (said !== null && accepted.length > 0 && !accepted.some((text) => sameWords(said, require_registry.fold(text)))) findings.push(require_registry.finding("media_transcript_differs", ["media", written$1(media.transcript) ? "transcript" : "script"], `The recording's transcript says different words from the item's \`${field}\`, which is what the recording is meant to say.`));
|
|
1092
|
+
return findings;
|
|
1093
|
+
}
|
|
1094
|
+
/** A group's material: its own transcript against its script, and its media's rules. */
|
|
1095
|
+
function stimulusFindings(stimulus) {
|
|
1096
|
+
if (!require_registry.isRecord(stimulus)) return [];
|
|
1097
|
+
return [...holderFindings({
|
|
1098
|
+
transcript: stimulus.transcript,
|
|
1099
|
+
script: stimulus.script
|
|
1100
|
+
}, ["stimulus"]), ...holderFindings(stimulus.media, ["stimulus", "media"])];
|
|
1101
|
+
}
|
|
1102
|
+
//#endregion
|
|
869
1103
|
//#region src/authoring/item-group.ts
|
|
870
1104
|
/**
|
|
871
1105
|
* A new, empty reading or listening testlet for an editor to start from.
|
|
@@ -963,7 +1197,11 @@ function createInteractiveVideoDraft(context) {
|
|
|
963
1197
|
function critiqueItemGroupDraft(draft) {
|
|
964
1198
|
if (!require_registry.isRecord(draft)) return [];
|
|
965
1199
|
const items = Array.isArray(draft.items) ? draft.items : [];
|
|
966
|
-
return require_registry.withValidationScope(() => [
|
|
1200
|
+
return require_registry.withValidationScope(() => [
|
|
1201
|
+
...stimulusFindings(draft.stimulus),
|
|
1202
|
+
...items.flatMap((item, index) => repathed(critiqueItem(item), ["items", index])),
|
|
1203
|
+
...require_registry.keyPositionFinding(items, ["items"])
|
|
1204
|
+
]);
|
|
967
1205
|
}
|
|
968
1206
|
/**
|
|
969
1207
|
* The item critic over a question set, as `<ActivitySequence>` takes one: each
|
|
@@ -1410,9 +1648,9 @@ function normalise(found) {
|
|
|
1410
1648
|
function critiqueDraft(type, draft) {
|
|
1411
1649
|
const descriptor = require_registry.getActivityTypeDescriptor(type);
|
|
1412
1650
|
if (descriptor === void 0) throw new require_registry.UnknownActivityTypeError(String(type));
|
|
1651
|
+
if (!require_registry.isRecord(draft)) return [];
|
|
1413
1652
|
const critique = descriptor.authoring?.critique;
|
|
1414
|
-
|
|
1415
|
-
return require_registry.withValidationScope(() => critique(draft)).map(require_registry.normaliseFinding);
|
|
1653
|
+
return require_registry.withValidationScope(() => [...critique === void 0 ? [] : critique(draft), ...recordingFindings(draft)]).map(require_registry.normaliseFinding);
|
|
1416
1654
|
}
|
|
1417
1655
|
//#endregion
|
|
1418
1656
|
//#region src/ai-critique.ts
|
|
@@ -1448,6 +1686,10 @@ const NOT_WORDS = /* @__PURE__ */ new Set([
|
|
|
1448
1686
|
"bankId",
|
|
1449
1687
|
"url",
|
|
1450
1688
|
"captionsUrl",
|
|
1689
|
+
"src",
|
|
1690
|
+
"srclang",
|
|
1691
|
+
"poster",
|
|
1692
|
+
"speaker",
|
|
1451
1693
|
"match",
|
|
1452
1694
|
"playback",
|
|
1453
1695
|
"recording",
|
|
@@ -2289,65 +2531,19 @@ function interactiveVideoFromDrafts(input) {
|
|
|
2289
2531
|
};
|
|
2290
2532
|
}
|
|
2291
2533
|
//#endregion
|
|
2292
|
-
//#region src/ai-
|
|
2534
|
+
//#region src/ai-written.ts
|
|
2535
|
+
/**
|
|
2536
|
+
* What the AI features about a written response share: reading a model's text
|
|
2537
|
+
* fields, and finding each correction's quote in the learner's text.
|
|
2538
|
+
*
|
|
2539
|
+
* Internal. `ai-writing.ts` (feedback on a draft) and `ai-grading.ts` (a
|
|
2540
|
+
* grade) are their public faces, and hold every correction to the same
|
|
2541
|
+
* anchoring.
|
|
2542
|
+
*/
|
|
2293
2543
|
/** The most corrections one piece of writing feedback may carry. More is refused, not cut. */
|
|
2294
2544
|
const AI_WRITING_MAX_CORRECTIONS = 20;
|
|
2295
2545
|
/** The longest quote, correction, explanation or criterion comment, in code points. */
|
|
2296
2546
|
const AI_WRITING_MAX_FIELD_LENGTH = 500;
|
|
2297
|
-
/** The longest `category` or `band`: a tag, not a sentence. */
|
|
2298
|
-
const MAX_TAG_LENGTH = 40;
|
|
2299
|
-
/**
|
|
2300
|
-
* The request for feedback on a draft, or `null` when there is none to give:
|
|
2301
|
-
* not a written response, an author who switched explanations off (writing
|
|
2302
|
-
* feedback reads `ai.explanations`), a redacted item — feedback is for
|
|
2303
|
-
* practice, where the item is whole — or a draft with nothing in it.
|
|
2304
|
-
*
|
|
2305
|
-
* The facts are the task as the learner saw it, the draft verbatim with its
|
|
2306
|
-
* word count recomputed as the grader will count it, and the rubric the answer
|
|
2307
|
-
* will be graded on. `previousFeedback` is the text of the feedback already
|
|
2308
|
-
* shown on earlier drafts, so a model can speak to what changed. `group` is the
|
|
2309
|
-
* item group the task sits in: its material, the text a summary is of, goes
|
|
2310
|
-
* with the request as `material`.
|
|
2311
|
-
*/
|
|
2312
|
-
function aiWritingFeedbackRequest(input) {
|
|
2313
|
-
const { data, response, learnerLocale } = input;
|
|
2314
|
-
if (!aiSupports(data.type, "writing-feedback") || !aiAllowedByContent(data, "writing-feedback") || data.redacted === true) return null;
|
|
2315
|
-
const text = response?.type === "written-response" ? response.text : "";
|
|
2316
|
-
if (typeof text !== "string" || text.trim() === "") return null;
|
|
2317
|
-
const item = data;
|
|
2318
|
-
const wordCount = require_registry.countWords(text);
|
|
2319
|
-
const minWords = typeof item.minWords === "number" ? item.minWords : 0;
|
|
2320
|
-
const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
|
|
2321
|
-
const criteria = Array.isArray(item.rubric?.criteria) ? item.rubric.criteria : null;
|
|
2322
|
-
const rubric = criteria === null || criteria.length === 0 ? null : criteria.map((criterion) => ({
|
|
2323
|
-
name: criterion.name,
|
|
2324
|
-
...typeof criterion.description === "string" ? { description: criterion.description } : {},
|
|
2325
|
-
weight: criterion.weight
|
|
2326
|
-
}));
|
|
2327
|
-
const previousFeedback = [...input.previousFeedback ?? []];
|
|
2328
|
-
const material = materialFacts(input.group);
|
|
2329
|
-
return {
|
|
2330
|
-
feature: "writing-feedback",
|
|
2331
|
-
facts: {
|
|
2332
|
-
activityType: "written-response",
|
|
2333
|
-
activityId: data.id,
|
|
2334
|
-
title: data.title,
|
|
2335
|
-
...typeof data.locale === "string" ? { locale: data.locale } : {},
|
|
2336
|
-
prompt: typeof item.prompt === "string" ? item.prompt : "",
|
|
2337
|
-
text,
|
|
2338
|
-
wordCount,
|
|
2339
|
-
minWords,
|
|
2340
|
-
maxWords,
|
|
2341
|
-
withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
|
|
2342
|
-
rubric,
|
|
2343
|
-
...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
|
|
2344
|
-
},
|
|
2345
|
-
draftNumber: previousFeedback.length + 1,
|
|
2346
|
-
previousFeedback,
|
|
2347
|
-
...material !== null ? { material } : {},
|
|
2348
|
-
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
2349
|
-
};
|
|
2350
|
-
}
|
|
2351
2547
|
/**
|
|
2352
2548
|
* Where `quote` sits in the draft: at `claimed`, when the model named a range —
|
|
2353
2549
|
* which must then hold the quote — or else at its first occurrence not already
|
|
@@ -2446,7 +2642,7 @@ function checkCorrections(raw, text) {
|
|
|
2446
2642
|
refusal: "malformed"
|
|
2447
2643
|
};
|
|
2448
2644
|
const explanation = textField(entry.explanation, 500);
|
|
2449
|
-
const category = textField(entry.category,
|
|
2645
|
+
const category = textField(entry.category, 40);
|
|
2450
2646
|
if (!explanation.ok) return explanation;
|
|
2451
2647
|
if (!category.ok) return category;
|
|
2452
2648
|
let claimed;
|
|
@@ -2478,6 +2674,385 @@ function checkCorrections(raw, text) {
|
|
|
2478
2674
|
corrections
|
|
2479
2675
|
};
|
|
2480
2676
|
}
|
|
2677
|
+
//#endregion
|
|
2678
|
+
//#region src/ai-grading.ts
|
|
2679
|
+
/**
|
|
2680
|
+
* How far a model's level probabilities may sum from 1 and still be a
|
|
2681
|
+
* distribution: rounding in a model's output, not a different judgement.
|
|
2682
|
+
*/
|
|
2683
|
+
const AI_GRADING_PROBABILITY_TOLERANCE = require_registry.LEVEL_PROBABILITY_TOLERANCE;
|
|
2684
|
+
/**
|
|
2685
|
+
* The request for a model's grade of a submitted written response, or why there
|
|
2686
|
+
* is none to make.
|
|
2687
|
+
*
|
|
2688
|
+
* Grading happens on the server, from the whole item: the rubric is what a
|
|
2689
|
+
* model is held to, so an item without one — or one whose criteria are not each
|
|
2690
|
+
* a name and a weight — gives no request, nor does a `redact()` projection or
|
|
2691
|
+
* an empty response. A criterion's `levels`, when there are any, must be ones
|
|
2692
|
+
* the grading functions can read; levels they cannot read give no request
|
|
2693
|
+
* rather than a grade on a scale the author did not mean. `group` is the item
|
|
2694
|
+
* group the task sits in: its material goes with the request as `material`, as
|
|
2695
|
+
* on feedback on writing.
|
|
2696
|
+
*
|
|
2697
|
+
* The author's AI switch (`ai.explanations`) is not read: grading is the
|
|
2698
|
+
* deployment's decision, made on the server, not help a learner asks for.
|
|
2699
|
+
*/
|
|
2700
|
+
function aiGradingRequest(input) {
|
|
2701
|
+
const { data, response, learnerLocale } = input;
|
|
2702
|
+
if (data.type !== "written-response") return {
|
|
2703
|
+
ok: false,
|
|
2704
|
+
problem: "not-written-response"
|
|
2705
|
+
};
|
|
2706
|
+
if (data.redacted === true) return {
|
|
2707
|
+
ok: false,
|
|
2708
|
+
problem: "redacted"
|
|
2709
|
+
};
|
|
2710
|
+
const item = data;
|
|
2711
|
+
const rubric = rubricFacts(item.rubric);
|
|
2712
|
+
if (!rubric.ok) return rubric;
|
|
2713
|
+
const text = response?.type === "written-response" ? response.text : void 0;
|
|
2714
|
+
if (typeof text !== "string" || text.trim() === "") return {
|
|
2715
|
+
ok: false,
|
|
2716
|
+
problem: "empty-response"
|
|
2717
|
+
};
|
|
2718
|
+
const wordCount = require_registry.countWords(text);
|
|
2719
|
+
const minWords = typeof item.minWords === "number" ? item.minWords : 0;
|
|
2720
|
+
const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
|
|
2721
|
+
const material = materialFacts(input.group);
|
|
2722
|
+
return {
|
|
2723
|
+
ok: true,
|
|
2724
|
+
request: {
|
|
2725
|
+
feature: "grading",
|
|
2726
|
+
facts: {
|
|
2727
|
+
activityType: "written-response",
|
|
2728
|
+
activityId: data.id,
|
|
2729
|
+
title: data.title,
|
|
2730
|
+
...typeof data.locale === "string" ? { locale: data.locale } : {},
|
|
2731
|
+
prompt: typeof item.prompt === "string" ? item.prompt : "",
|
|
2732
|
+
text,
|
|
2733
|
+
wordCount,
|
|
2734
|
+
minWords,
|
|
2735
|
+
maxWords,
|
|
2736
|
+
withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
|
|
2737
|
+
rubric: rubric.criteria,
|
|
2738
|
+
...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
|
|
2739
|
+
},
|
|
2740
|
+
...material !== null ? { material } : {},
|
|
2741
|
+
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
/** The rubric as a model is given it, each criterion's levels read. */
|
|
2746
|
+
function rubricFacts(rubric) {
|
|
2747
|
+
const listed = isRecord(rubric) ? rubric.criteria : void 0;
|
|
2748
|
+
if (!Array.isArray(listed) || listed.length === 0) return {
|
|
2749
|
+
ok: false,
|
|
2750
|
+
problem: "no-rubric"
|
|
2751
|
+
};
|
|
2752
|
+
const criteria = [];
|
|
2753
|
+
const names = /* @__PURE__ */ new Set();
|
|
2754
|
+
for (const [index, criterion] of listed.entries()) {
|
|
2755
|
+
if (!isRecord(criterion)) return {
|
|
2756
|
+
ok: false,
|
|
2757
|
+
problem: "no-rubric",
|
|
2758
|
+
path: [
|
|
2759
|
+
"rubric",
|
|
2760
|
+
"criteria",
|
|
2761
|
+
index
|
|
2762
|
+
]
|
|
2763
|
+
};
|
|
2764
|
+
const { name, description, weight } = criterion;
|
|
2765
|
+
if (typeof name !== "string" || name.trim() === "" || typeof weight !== "number") return {
|
|
2766
|
+
ok: false,
|
|
2767
|
+
problem: "no-rubric",
|
|
2768
|
+
path: [
|
|
2769
|
+
"rubric",
|
|
2770
|
+
"criteria",
|
|
2771
|
+
index
|
|
2772
|
+
]
|
|
2773
|
+
};
|
|
2774
|
+
if (names.has(require_registry.levelKey(name))) return {
|
|
2775
|
+
ok: false,
|
|
2776
|
+
problem: "duplicate-criteria",
|
|
2777
|
+
path: [
|
|
2778
|
+
"rubric",
|
|
2779
|
+
"criteria",
|
|
2780
|
+
index,
|
|
2781
|
+
"name"
|
|
2782
|
+
]
|
|
2783
|
+
};
|
|
2784
|
+
names.add(require_registry.levelKey(name));
|
|
2785
|
+
const levels = require_registry.readRubricLevels(criterion.levels);
|
|
2786
|
+
if (!levels.ok) return {
|
|
2787
|
+
ok: false,
|
|
2788
|
+
problem: "unreadable-levels",
|
|
2789
|
+
path: [
|
|
2790
|
+
"rubric",
|
|
2791
|
+
"criteria",
|
|
2792
|
+
index,
|
|
2793
|
+
"levels"
|
|
2794
|
+
]
|
|
2795
|
+
};
|
|
2796
|
+
criteria.push({
|
|
2797
|
+
name,
|
|
2798
|
+
...typeof description === "string" ? { description } : {},
|
|
2799
|
+
weight,
|
|
2800
|
+
levels: levels.levels
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
return {
|
|
2804
|
+
ok: true,
|
|
2805
|
+
criteria
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* The rubric's criteria by name, folded as level labels are: composed, trimmed,
|
|
2810
|
+
* spaced once, lower-cased. A model that writes "task achievement" for "Task
|
|
2811
|
+
* achievement" judged that criterion. The request refused a rubric with two
|
|
2812
|
+
* names alike once folded, so a folded name is one criterion's alone.
|
|
2813
|
+
*/
|
|
2814
|
+
function criterionIndex(rubric) {
|
|
2815
|
+
const folded = new Map(rubric.map((criterion, index) => [require_registry.levelKey(criterion.name), index]));
|
|
2816
|
+
return (name) => folded.get(require_registry.levelKey(name));
|
|
2817
|
+
}
|
|
2818
|
+
/** A finite number. */
|
|
2819
|
+
const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
2820
|
+
/**
|
|
2821
|
+
* One criterion's judgement, on that criterion's scale: a level it has, with
|
|
2822
|
+
* any probabilities a distribution over its levels whose likeliest is the
|
|
2823
|
+
* level chosen; or, without levels, a score from 0 to its `maxScore` (1 by
|
|
2824
|
+
* default).
|
|
2825
|
+
*/
|
|
2826
|
+
function judgeCriterion(entry, criterion) {
|
|
2827
|
+
const { level, score, maxScore, probabilities } = entry;
|
|
2828
|
+
const levels = criterion.levels;
|
|
2829
|
+
if (levels === null) {
|
|
2830
|
+
if (level !== void 0 || probabilities !== void 0) return {
|
|
2831
|
+
ok: false,
|
|
2832
|
+
refusal: "out-of-scale"
|
|
2833
|
+
};
|
|
2834
|
+
if (score === void 0) return {
|
|
2835
|
+
ok: false,
|
|
2836
|
+
refusal: "misses-criteria"
|
|
2837
|
+
};
|
|
2838
|
+
if (!isNumber(score) || maxScore !== void 0 && !isNumber(maxScore)) return {
|
|
2839
|
+
ok: false,
|
|
2840
|
+
refusal: "malformed"
|
|
2841
|
+
};
|
|
2842
|
+
const max = maxScore ?? 1;
|
|
2843
|
+
if (!(max > 0) || score < 0 || score - max > max * 1e-9) return {
|
|
2844
|
+
ok: false,
|
|
2845
|
+
refusal: "out-of-scale"
|
|
2846
|
+
};
|
|
2847
|
+
return {
|
|
2848
|
+
ok: true,
|
|
2849
|
+
judged: {
|
|
2850
|
+
name: criterion.name,
|
|
2851
|
+
score: Math.min(1, score / max)
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
if (score !== void 0 || maxScore !== void 0) return {
|
|
2856
|
+
ok: false,
|
|
2857
|
+
refusal: "out-of-scale"
|
|
2858
|
+
};
|
|
2859
|
+
if (level === void 0) return {
|
|
2860
|
+
ok: false,
|
|
2861
|
+
refusal: "misses-criteria"
|
|
2862
|
+
};
|
|
2863
|
+
if (typeof level !== "string") return {
|
|
2864
|
+
ok: false,
|
|
2865
|
+
refusal: "malformed"
|
|
2866
|
+
};
|
|
2867
|
+
const chosen = levels.findIndex((one) => require_registry.levelKey(one.label) === require_registry.levelKey(level));
|
|
2868
|
+
if (chosen === -1) return {
|
|
2869
|
+
ok: false,
|
|
2870
|
+
refusal: "out-of-scale"
|
|
2871
|
+
};
|
|
2872
|
+
const judged = {
|
|
2873
|
+
name: criterion.name,
|
|
2874
|
+
level: levels[chosen].label,
|
|
2875
|
+
score: chosen / (levels.length - 1)
|
|
2876
|
+
};
|
|
2877
|
+
if (probabilities === void 0) return {
|
|
2878
|
+
ok: true,
|
|
2879
|
+
judged
|
|
2880
|
+
};
|
|
2881
|
+
if (!Array.isArray(probabilities)) return {
|
|
2882
|
+
ok: false,
|
|
2883
|
+
refusal: "malformed"
|
|
2884
|
+
};
|
|
2885
|
+
const values = require_registry.levelDistribution(probabilities, levels.length, chosen);
|
|
2886
|
+
if (values === null) return {
|
|
2887
|
+
ok: false,
|
|
2888
|
+
refusal: "out-of-scale"
|
|
2889
|
+
};
|
|
2890
|
+
return {
|
|
2891
|
+
ok: true,
|
|
2892
|
+
judged: {
|
|
2893
|
+
...judged,
|
|
2894
|
+
probabilities: values
|
|
2895
|
+
}
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Checks a grade a port returned for `request`. Whole or nothing, as every AI
|
|
2900
|
+
* text is: a reply the SDK doubts in any part is refused, and is no grade —
|
|
2901
|
+
* re-ask the model or send the response to a person; never record a 0.
|
|
2902
|
+
*
|
|
2903
|
+
* - **Every criterion of the rubric, once** (`misses-criteria`). Names match as
|
|
2904
|
+
* written, or folded when the folded name is one criterion's alone. Leaving
|
|
2905
|
+
* one out, inventing or renaming one, judging one twice or calling one not
|
|
2906
|
+
* applicable refuses the reply: a total renormalised over fewer criteria
|
|
2907
|
+
* is a grade of a different task.
|
|
2908
|
+
* - **On each criterion's own scale** (`out-of-scale`). With levels: a `level`
|
|
2909
|
+
* the criterion has, as labelled (case and spacing aside), and any
|
|
2910
|
+
* `probabilities` a distribution over its levels, lowest first, whose
|
|
2911
|
+
* likeliest is the level chosen. Without levels: a `score` from 0 to its
|
|
2912
|
+
* `maxScore`, 1 by default.
|
|
2913
|
+
* - **Every correction quotes the response** (`misquotes-answer`), anchored as
|
|
2914
|
+
* feedback on writing anchors it.
|
|
2915
|
+
* - **Text fields** — a criterion's `comment` (at most
|
|
2916
|
+
* `AI_WRITING_MAX_FIELD_LENGTH` code points), `feedback` and `rationale`
|
|
2917
|
+
* (at most `AI_TEXT_MAX_LENGTH`) — are plain text, cleaned; longer is
|
|
2918
|
+
* `too-long`. `requiresHumanReview` is a boolean.
|
|
2919
|
+
*
|
|
2920
|
+
* The total is not computed here: `gradeFromJudgement` computes it, as
|
|
2921
|
+
* `gradeFromRubric` computes every grade in the SDK.
|
|
2922
|
+
*/
|
|
2923
|
+
function checkAiGrading(raw, request) {
|
|
2924
|
+
if (!isRecord(raw)) return {
|
|
2925
|
+
ok: false,
|
|
2926
|
+
refusal: "malformed"
|
|
2927
|
+
};
|
|
2928
|
+
const { criteria: listed, feedback, corrections, rationale, requiresHumanReview } = raw;
|
|
2929
|
+
if (listed === void 0) return {
|
|
2930
|
+
ok: false,
|
|
2931
|
+
refusal: "misses-criteria"
|
|
2932
|
+
};
|
|
2933
|
+
if (!Array.isArray(listed)) return {
|
|
2934
|
+
ok: false,
|
|
2935
|
+
refusal: "malformed"
|
|
2936
|
+
};
|
|
2937
|
+
const rubric = request.facts.rubric;
|
|
2938
|
+
const indexOf = criterionIndex(rubric);
|
|
2939
|
+
const judged = rubric.map(() => void 0);
|
|
2940
|
+
for (const entry of listed) {
|
|
2941
|
+
if (!isRecord(entry)) return {
|
|
2942
|
+
ok: false,
|
|
2943
|
+
refusal: "malformed"
|
|
2944
|
+
};
|
|
2945
|
+
const name = entry.name;
|
|
2946
|
+
const notApplicable = entry.notApplicable;
|
|
2947
|
+
const written = entry.comment;
|
|
2948
|
+
const scale = {
|
|
2949
|
+
level: entry.level,
|
|
2950
|
+
score: entry.score,
|
|
2951
|
+
maxScore: entry.maxScore,
|
|
2952
|
+
probabilities: entry.probabilities
|
|
2953
|
+
};
|
|
2954
|
+
if (typeof name !== "string" || notApplicable !== void 0 && typeof notApplicable !== "boolean") return {
|
|
2955
|
+
ok: false,
|
|
2956
|
+
refusal: "malformed"
|
|
2957
|
+
};
|
|
2958
|
+
const index = indexOf(name);
|
|
2959
|
+
if (index === void 0 || judged[index] !== void 0 || notApplicable === true) return {
|
|
2960
|
+
ok: false,
|
|
2961
|
+
refusal: "misses-criteria"
|
|
2962
|
+
};
|
|
2963
|
+
const criterion = rubric[index];
|
|
2964
|
+
const scaled = judgeCriterion(scale, criterion);
|
|
2965
|
+
if (!scaled.ok) return scaled;
|
|
2966
|
+
const comment = textField(written, 500);
|
|
2967
|
+
if (!comment.ok) return comment;
|
|
2968
|
+
judged[index] = {
|
|
2969
|
+
...scaled.judged,
|
|
2970
|
+
...comment.value !== void 0 ? { comment: comment.value } : {}
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
if (judged.some((one) => one === void 0)) return {
|
|
2974
|
+
ok: false,
|
|
2975
|
+
refusal: "misses-criteria"
|
|
2976
|
+
};
|
|
2977
|
+
const anchored = checkCorrections(corrections, request.facts.text);
|
|
2978
|
+
if (!anchored.ok) return anchored;
|
|
2979
|
+
const said = textField(feedback, AI_TEXT_MAX_LENGTH);
|
|
2980
|
+
if (!said.ok) return said;
|
|
2981
|
+
const why = textField(rationale, AI_TEXT_MAX_LENGTH);
|
|
2982
|
+
if (!why.ok) return why;
|
|
2983
|
+
if (requiresHumanReview !== void 0 && typeof requiresHumanReview !== "boolean") return {
|
|
2984
|
+
ok: false,
|
|
2985
|
+
refusal: "malformed"
|
|
2986
|
+
};
|
|
2987
|
+
const provenance = provenanceOf(raw.provenance);
|
|
2988
|
+
const usage = usageOf(raw.usage);
|
|
2989
|
+
return {
|
|
2990
|
+
ok: true,
|
|
2991
|
+
judgement: {
|
|
2992
|
+
criteria: judged,
|
|
2993
|
+
feedback: said.value ?? null,
|
|
2994
|
+
corrections: anchored.corrections,
|
|
2995
|
+
...why.value !== void 0 ? { rationale: why.value } : {},
|
|
2996
|
+
requiresHumanReview: requiresHumanReview === true,
|
|
2997
|
+
...provenance !== void 0 ? { provenance } : {},
|
|
2998
|
+
...usage !== void 0 ? { usage } : {}
|
|
2999
|
+
}
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
//#endregion
|
|
3003
|
+
//#region src/ai-writing.ts
|
|
3004
|
+
/**
|
|
3005
|
+
* The request for feedback on a draft, or `null` when there is none to give:
|
|
3006
|
+
* not a written response, an author who switched explanations off (writing
|
|
3007
|
+
* feedback reads `ai.explanations`), a redacted item — feedback is for
|
|
3008
|
+
* practice, where the item is whole — or a draft with nothing in it.
|
|
3009
|
+
*
|
|
3010
|
+
* The facts are the task as the learner saw it, the draft verbatim with its
|
|
3011
|
+
* word count recomputed as the grader will count it, and the rubric the answer
|
|
3012
|
+
* will be graded on. `previousFeedback` is the text of the feedback already
|
|
3013
|
+
* shown on earlier drafts, so a model can speak to what changed. `group` is the
|
|
3014
|
+
* item group the task sits in: its material, the text a summary is of, goes
|
|
3015
|
+
* with the request as `material`.
|
|
3016
|
+
*/
|
|
3017
|
+
function aiWritingFeedbackRequest(input) {
|
|
3018
|
+
const { data, response, learnerLocale } = input;
|
|
3019
|
+
if (!aiSupports(data.type, "writing-feedback") || !aiAllowedByContent(data, "writing-feedback") || data.redacted === true) return null;
|
|
3020
|
+
const text = response?.type === "written-response" ? response.text : "";
|
|
3021
|
+
if (typeof text !== "string" || text.trim() === "") return null;
|
|
3022
|
+
const item = data;
|
|
3023
|
+
const wordCount = require_registry.countWords(text);
|
|
3024
|
+
const minWords = typeof item.minWords === "number" ? item.minWords : 0;
|
|
3025
|
+
const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
|
|
3026
|
+
const criteria = Array.isArray(item.rubric?.criteria) ? item.rubric.criteria : null;
|
|
3027
|
+
const rubric = criteria === null || criteria.length === 0 ? null : criteria.map((criterion) => ({
|
|
3028
|
+
name: criterion.name,
|
|
3029
|
+
...typeof criterion.description === "string" ? { description: criterion.description } : {},
|
|
3030
|
+
weight: criterion.weight
|
|
3031
|
+
}));
|
|
3032
|
+
const previousFeedback = [...input.previousFeedback ?? []];
|
|
3033
|
+
const material = materialFacts(input.group);
|
|
3034
|
+
return {
|
|
3035
|
+
feature: "writing-feedback",
|
|
3036
|
+
facts: {
|
|
3037
|
+
activityType: "written-response",
|
|
3038
|
+
activityId: data.id,
|
|
3039
|
+
title: data.title,
|
|
3040
|
+
...typeof data.locale === "string" ? { locale: data.locale } : {},
|
|
3041
|
+
prompt: typeof item.prompt === "string" ? item.prompt : "",
|
|
3042
|
+
text,
|
|
3043
|
+
wordCount,
|
|
3044
|
+
minWords,
|
|
3045
|
+
maxWords,
|
|
3046
|
+
withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
|
|
3047
|
+
rubric,
|
|
3048
|
+
...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
|
|
3049
|
+
},
|
|
3050
|
+
draftNumber: previousFeedback.length + 1,
|
|
3051
|
+
previousFeedback,
|
|
3052
|
+
...material !== null ? { material } : {},
|
|
3053
|
+
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
2481
3056
|
/** A criterion score a grade could be computed from: finite, 0 or more, and not above its maximum. */
|
|
2482
3057
|
function isJudgedScore(score, maxScore) {
|
|
2483
3058
|
if (score === void 0) return maxScore === void 0 || typeof maxScore === "number" && maxScore > 0;
|
|
@@ -2524,7 +3099,7 @@ function checkCriteria(raw, rubric) {
|
|
|
2524
3099
|
ok: false,
|
|
2525
3100
|
refusal: "malformed"
|
|
2526
3101
|
};
|
|
2527
|
-
const band = textField(entry.band,
|
|
3102
|
+
const band = textField(entry.band, 40);
|
|
2528
3103
|
const comment = textField(entry.comment, 500);
|
|
2529
3104
|
if (!band.ok) return band;
|
|
2530
3105
|
if (!comment.ok) return comment;
|
|
@@ -2638,6 +3213,12 @@ Object.defineProperty(exports, "AI_EXPLANATION_TYPES", {
|
|
|
2638
3213
|
return AI_EXPLANATION_TYPES;
|
|
2639
3214
|
}
|
|
2640
3215
|
});
|
|
3216
|
+
Object.defineProperty(exports, "AI_GRADING_PROBABILITY_TOLERANCE", {
|
|
3217
|
+
enumerable: true,
|
|
3218
|
+
get: function() {
|
|
3219
|
+
return AI_GRADING_PROBABILITY_TOLERANCE;
|
|
3220
|
+
}
|
|
3221
|
+
});
|
|
2641
3222
|
Object.defineProperty(exports, "AI_HINT_TYPES", {
|
|
2642
3223
|
enumerable: true,
|
|
2643
3224
|
get: function() {
|
|
@@ -2710,6 +3291,12 @@ Object.defineProperty(exports, "aiGradeOf", {
|
|
|
2710
3291
|
return aiGradeOf;
|
|
2711
3292
|
}
|
|
2712
3293
|
});
|
|
3294
|
+
Object.defineProperty(exports, "aiGradingRequest", {
|
|
3295
|
+
enumerable: true,
|
|
3296
|
+
get: function() {
|
|
3297
|
+
return aiGradingRequest;
|
|
3298
|
+
}
|
|
3299
|
+
});
|
|
2713
3300
|
Object.defineProperty(exports, "aiHintRequest", {
|
|
2714
3301
|
enumerable: true,
|
|
2715
3302
|
get: function() {
|
|
@@ -2758,6 +3345,12 @@ Object.defineProperty(exports, "checkAiExplanation", {
|
|
|
2758
3345
|
return checkAiExplanation;
|
|
2759
3346
|
}
|
|
2760
3347
|
});
|
|
3348
|
+
Object.defineProperty(exports, "checkAiGrading", {
|
|
3349
|
+
enumerable: true,
|
|
3350
|
+
get: function() {
|
|
3351
|
+
return checkAiGrading;
|
|
3352
|
+
}
|
|
3353
|
+
});
|
|
2761
3354
|
Object.defineProperty(exports, "checkAiHint", {
|
|
2762
3355
|
enumerable: true,
|
|
2763
3356
|
get: function() {
|
|
@@ -2824,12 +3417,30 @@ Object.defineProperty(exports, "interactiveVideoFromDrafts", {
|
|
|
2824
3417
|
return interactiveVideoFromDrafts;
|
|
2825
3418
|
}
|
|
2826
3419
|
});
|
|
3420
|
+
Object.defineProperty(exports, "listMediaRefs", {
|
|
3421
|
+
enumerable: true,
|
|
3422
|
+
get: function() {
|
|
3423
|
+
return listMediaRefs;
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
3426
|
+
Object.defineProperty(exports, "mapMediaRefs", {
|
|
3427
|
+
enumerable: true,
|
|
3428
|
+
get: function() {
|
|
3429
|
+
return mapMediaRefs;
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
2827
3432
|
Object.defineProperty(exports, "readAiTextResult", {
|
|
2828
3433
|
enumerable: true,
|
|
2829
3434
|
get: function() {
|
|
2830
3435
|
return readAiTextResult;
|
|
2831
3436
|
}
|
|
2832
3437
|
});
|
|
3438
|
+
Object.defineProperty(exports, "transcriptOf", {
|
|
3439
|
+
enumerable: true,
|
|
3440
|
+
get: function() {
|
|
3441
|
+
return transcriptOf;
|
|
3442
|
+
}
|
|
3443
|
+
});
|
|
2833
3444
|
Object.defineProperty(exports, "validateDraft", {
|
|
2834
3445
|
enumerable: true,
|
|
2835
3446
|
get: function() {
|
|
@@ -2843,4 +3454,4 @@ Object.defineProperty(exports, "validateItemGroupDraft", {
|
|
|
2843
3454
|
}
|
|
2844
3455
|
});
|
|
2845
3456
|
|
|
2846
|
-
//# sourceMappingURL=ai-writing-
|
|
3457
|
+
//# sourceMappingURL=ai-writing-C4YRVrFi.cjs.map
|