@intellectif/lk-core 1.5.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 +57 -0
- package/dist/ai-check.cjs +2 -2
- package/dist/ai-check.d.cts +1 -1
- package/dist/ai-check.d.ts +1 -1
- package/dist/ai-check.js +2 -2
- package/dist/{ai-BYryFkF9.d.ts → ai-i3jwXnpX.d.cts} +45 -4
- package/dist/ai-i3jwXnpX.d.cts.map +1 -0
- package/dist/{ai-BYryFkF9.d.cts → ai-i3jwXnpX.d.ts} +45 -4
- package/dist/ai-i3jwXnpX.d.ts.map +1 -0
- package/dist/{ai-writing-Cen7-3qs.cjs → ai-writing-C4YRVrFi.cjs} +275 -15
- package/dist/ai-writing-C4YRVrFi.cjs.map +1 -0
- package/dist/{ai-writing-D8PdhJJz.js → ai-writing-OJ5m_1EB.js} +273 -31
- package/dist/ai-writing-OJ5m_1EB.js.map +1 -0
- package/dist/{index-D7R3Ll8R.d.cts → index--U7hHV0s.d.cts} +2 -2
- package/dist/{index-D7R3Ll8R.d.cts.map → index--U7hHV0s.d.cts.map} +1 -1
- package/dist/{index-qMbxed1B.d.cts → index-CFRgZLhH.d.cts} +41 -12
- package/dist/index-CFRgZLhH.d.cts.map +1 -0
- package/dist/{index-DUaW1Tj5.d.ts → index-DeD8EI1D.d.ts} +2 -2
- package/dist/{index-DUaW1Tj5.d.ts.map → index-DeD8EI1D.d.ts.map} +1 -1
- package/dist/{index-wMc1E_aM.d.ts → index-Dj92T76o.d.ts} +2 -2
- package/dist/{index-wMc1E_aM.d.ts.map → index-Dj92T76o.d.ts.map} +1 -1
- package/dist/{index-Bg0aeWtM.d.cts → index-Uh1u57yq.d.cts} +2 -2
- package/dist/{index-Bg0aeWtM.d.cts.map → index-Uh1u57yq.d.cts.map} +1 -1
- package/dist/{index-CxpiWedW.d.ts → index-WBHSrIUU.d.ts} +41 -12
- package/dist/index-WBHSrIUU.d.ts.map +1 -0
- package/dist/index.cjs +63 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -5
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +137 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -8
- package/dist/index.js.map +1 -1
- package/dist/{item-group-DdSUCUjM.js → item-group-BbvvH_Ob.js} +3 -2
- package/dist/item-group-BbvvH_Ob.js.map +1 -0
- package/dist/{item-group-DEB7nLID.cjs → item-group-CQRbb8Rx.cjs} +3 -2
- package/dist/item-group-CQRbb8Rx.cjs.map +1 -0
- package/dist/{registry-DaZRpqjW.js → registry-B23o9UWh.js} +65 -18
- package/dist/registry-B23o9UWh.js.map +1 -0
- package/dist/{registry-Ce00A_OW.cjs → registry-Ih5n4PVZ.cjs} +82 -17
- package/dist/registry-Ih5n4PVZ.cjs.map +1 -0
- package/dist/{schemas-DksueYsu.cjs → schemas-Q2dyOgc2.cjs} +77 -14
- package/dist/schemas-Q2dyOgc2.cjs.map +1 -0
- package/dist/{schemas-BioKFf3F.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-BKNNTz-s.js → scoring-CLMdk8i9.js} +2 -2
- package/dist/{scoring-BKNNTz-s.js.map → scoring-CLMdk8i9.js.map} +1 -1
- package/dist/{scoring-lKQVYsxy.cjs → scoring-CaPS6q8b.cjs} +2 -2
- package/dist/{scoring-lKQVYsxy.cjs.map → scoring-CaPS6q8b.cjs.map} +1 -1
- package/dist/scoring.cjs +2 -2
- package/dist/scoring.d.cts +2 -2
- package/dist/scoring.d.ts +2 -2
- package/dist/scoring.js +2 -2
- package/dist/{xapi-NYrTlpID.cjs → xapi-BX0hvRRB.cjs} +2 -2
- package/dist/{xapi-NYrTlpID.cjs.map → xapi-BX0hvRRB.cjs.map} +1 -1
- package/dist/{xapi-wR6gu4qS.js → xapi-Cdk9fTvV.js} +2 -2
- package/dist/{xapi-wR6gu4qS.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 -1
- package/package.json +1 -1
- package/dist/ai-BYryFkF9.d.cts.map +0 -1
- package/dist/ai-BYryFkF9.d.ts.map +0 -1
- package/dist/ai-writing-Cen7-3qs.cjs.map +0 -1
- package/dist/ai-writing-D8PdhJJz.js.map +0 -1
- package/dist/index-CxpiWedW.d.ts.map +0 -1
- package/dist/index-qMbxed1B.d.cts.map +0 -1
- package/dist/item-group-DEB7nLID.cjs.map +0 -1
- package/dist/item-group-DdSUCUjM.js.map +0 -1
- package/dist/registry-Ce00A_OW.cjs.map +0 -1
- package/dist/registry-DaZRpqjW.js.map +0 -1
- package/dist/schemas-BioKFf3F.js.map +0 -1
- package/dist/schemas-DksueYsu.cjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { B as
|
|
2
|
-
import { F as isGradeInRange, _ as gradeFromRubric, c as validateSpeechAssessment, r as score, s as alignReadAloud } from "./scoring-
|
|
3
|
-
import { a as INTERACTIVE_VIDEO_ITEM_TYPES, i as validateItemGroup, t as ItemGroupSchema, u as isInteractiveVideoItemType } from "./item-group-
|
|
1
|
+
import { A as normaliseFinding, B as coveredPathsOf, F as levelKey, G as isUnwritten, H as isMissingId, I as readRubricLevels, J as refusesEmpty, K as issue, L as countWords, N as LEVEL_PROBABILITY_TOLERANCE, O as keyPositionFinding, P as levelDistribution, R as DRAFT_ISSUE_SEVERITY, U as isRecord$6, V as covers, W as isUnset, X as GROUP_CAPTIONS_REVEAL_DICTATION, Z as groupCaptionsField, _t as containsWords, a as readSchema, bt as placeholderOrder, k as finding, pt as withValidationScope, q as pathKey, rt as alignDictation, t as getActivityTypeDescriptor, tt as UnknownActivityTypeError, vt as fold, xt as revealsAnswer, yt as numberedPassage, z as checkMedia } from "./registry-B23o9UWh.js";
|
|
2
|
+
import { F as isGradeInRange, _ as gradeFromRubric, c as validateSpeechAssessment, r as score, s as alignReadAloud } from "./scoring-CLMdk8i9.js";
|
|
3
|
+
import { a as INTERACTIVE_VIDEO_ITEM_TYPES, i as validateItemGroup, t as ItemGroupSchema, u as isInteractiveVideoItemType } from "./item-group-BbvvH_Ob.js";
|
|
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" ? 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) => isRecord$6(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 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 ? 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 (!isRecord$6(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(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, fold(turns.map((turn) => turn.text).join(" ")))) findings.push(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 || !isRecord$6(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, fold(text)))) findings.push(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 (!isRecord$6(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.
|
|
@@ -961,9 +1195,13 @@ function createInteractiveVideoDraft(context) {
|
|
|
961
1195
|
* {@link validateItemGroupDraft} already reports it.
|
|
962
1196
|
*/
|
|
963
1197
|
function critiqueItemGroupDraft(draft) {
|
|
964
|
-
if (!isRecord$
|
|
1198
|
+
if (!isRecord$6(draft)) return [];
|
|
965
1199
|
const items = Array.isArray(draft.items) ? draft.items : [];
|
|
966
|
-
return withValidationScope(() => [
|
|
1200
|
+
return withValidationScope(() => [
|
|
1201
|
+
...stimulusFindings(draft.stimulus),
|
|
1202
|
+
...items.flatMap((item, index) => repathed(critiqueItem(item), ["items", index])),
|
|
1203
|
+
...keyPositionFinding(items, ["items"])
|
|
1204
|
+
]);
|
|
967
1205
|
}
|
|
968
1206
|
/**
|
|
969
1207
|
* The item critic over a question set, as `<ActivitySequence>` takes one: each
|
|
@@ -973,11 +1211,11 @@ function critiqueItemGroupDraft(draft) {
|
|
|
973
1211
|
* skipped.
|
|
974
1212
|
*/
|
|
975
1213
|
function critiqueDrafts(entries) {
|
|
976
|
-
return withValidationScope(() => [...entries.flatMap((entry, index) => repathed(isRecord$
|
|
1214
|
+
return withValidationScope(() => [...entries.flatMap((entry, index) => repathed(isRecord$6(entry) && entry.type === "item-group" ? critiqueItemGroupDraft(entry) : critiqueItem(entry), [index])), ...keyPositionFinding(entries, [])]);
|
|
977
1215
|
}
|
|
978
1216
|
/** One item's findings, or none for an item the critic cannot place. */
|
|
979
1217
|
function critiqueItem(item) {
|
|
980
|
-
if (!isRecord$
|
|
1218
|
+
if (!isRecord$6(item) || typeof item.type !== "string") return [];
|
|
981
1219
|
if (getActivityTypeDescriptor(item.type) === void 0) return [];
|
|
982
1220
|
return critiqueDraft(item.type, item);
|
|
983
1221
|
}
|
|
@@ -990,7 +1228,7 @@ function validateItemGroupDraft(draft) {
|
|
|
990
1228
|
}
|
|
991
1229
|
function validateItemGroupDraftInScope(draft) {
|
|
992
1230
|
const issues = [];
|
|
993
|
-
if (!isRecord$
|
|
1231
|
+
if (!isRecord$6(draft)) {
|
|
994
1232
|
issues.push(issue("ig_not_an_object", [], "A group must be an object with a stimulus and its items."));
|
|
995
1233
|
return {
|
|
996
1234
|
status: "invalid",
|
|
@@ -998,7 +1236,7 @@ function validateItemGroupDraftInScope(draft) {
|
|
|
998
1236
|
};
|
|
999
1237
|
}
|
|
1000
1238
|
issues.push(...checkGroupIdentity(draft));
|
|
1001
|
-
if (isRecord$
|
|
1239
|
+
if (isRecord$6(draft.stimulus)) issues.push(...checkStimulus(draft.stimulus));
|
|
1002
1240
|
else if (isUnset(draft.stimulus)) issues.push(issue("ig_stimulus_required", ["stimulus"], "Add the passage, recording or image the questions are about."));
|
|
1003
1241
|
issues.push(...checkItems(draft));
|
|
1004
1242
|
if (draft.timeline !== void 0 && draft.timeline !== null) issues.push(...checkTimeline(draft));
|
|
@@ -1048,10 +1286,10 @@ function validateItemGroupDraftInScope(draft) {
|
|
|
1048
1286
|
function checkTimeline(draft) {
|
|
1049
1287
|
const issues = [];
|
|
1050
1288
|
const timeline = draft.timeline;
|
|
1051
|
-
if (!isRecord$
|
|
1052
|
-
const stimulus = isRecord$
|
|
1289
|
+
if (!isRecord$6(timeline)) return issues;
|
|
1290
|
+
const stimulus = isRecord$6(draft.stimulus) ? draft.stimulus : void 0;
|
|
1053
1291
|
if (stimulus !== void 0 && !isUnwritten(stimulus.kind) && stimulus.kind !== "video") issues.push(issue("ig_timeline_stimulus_kind", ["stimulus", "kind"], "An interactive video needs a video: quizzes open at moments of it."));
|
|
1054
|
-
else if (stimulus !== void 0 && isRecord$
|
|
1292
|
+
else if (stimulus !== void 0 && isRecord$6(stimulus.media) && !isUnwritten(stimulus.media.type) && stimulus.media.type !== "video") issues.push(issue("ig_timeline_stimulus_kind", [
|
|
1055
1293
|
"stimulus",
|
|
1056
1294
|
"media",
|
|
1057
1295
|
"type"
|
|
@@ -1060,7 +1298,7 @@ function checkTimeline(draft) {
|
|
|
1060
1298
|
const items = Array.isArray(draft.items) ? draft.items : [];
|
|
1061
1299
|
const itemIndexById = /* @__PURE__ */ new Map();
|
|
1062
1300
|
items.forEach((item, index) => {
|
|
1063
|
-
if (!isRecord$
|
|
1301
|
+
if (!isRecord$6(item)) return;
|
|
1064
1302
|
if (typeof item.id === "string" && item.id !== "") itemIndexById.set(item.id, index);
|
|
1065
1303
|
const type = item.type;
|
|
1066
1304
|
if (typeof type === "string" && type !== "" && type !== "item-group" && !isInteractiveVideoItemType(type) && getActivityTypeDescriptor(type) !== void 0) issues.push(issue("ig_timeline_item_type", [
|
|
@@ -1078,7 +1316,7 @@ function checkTimeline(draft) {
|
|
|
1078
1316
|
const quizIds = /* @__PURE__ */ new Set();
|
|
1079
1317
|
const placed = /* @__PURE__ */ new Set();
|
|
1080
1318
|
cues.forEach((cue, cueIndex) => {
|
|
1081
|
-
if (!isRecord$
|
|
1319
|
+
if (!isRecord$6(cue)) return;
|
|
1082
1320
|
const at = (field, ...rest) => [
|
|
1083
1321
|
"timeline",
|
|
1084
1322
|
"cues",
|
|
@@ -1109,7 +1347,7 @@ function checkTimeline(draft) {
|
|
|
1109
1347
|
const chapters = Array.isArray(timeline.chapters) ? timeline.chapters : [];
|
|
1110
1348
|
let previousAt;
|
|
1111
1349
|
chapters.forEach((chapter, index) => {
|
|
1112
|
-
if (!isRecord$
|
|
1350
|
+
if (!isRecord$6(chapter)) return;
|
|
1113
1351
|
if (isUnwritten(chapter.title)) issues.push(issue("ig_timeline_chapter_title", [
|
|
1114
1352
|
"timeline",
|
|
1115
1353
|
"chapters",
|
|
@@ -1170,14 +1408,14 @@ function checkStimulus(stimulus) {
|
|
|
1170
1408
|
if (isUnwritten(kind)) issues.push(issue("ig_stimulus_kind_required", at("kind"), "Choose what the questions are about: a text, a recording, a video, an image, or a mix."));
|
|
1171
1409
|
else if (typeof kind !== "string" || !KINDS$1.has(kind)) {
|
|
1172
1410
|
if (typeof kind === "string") issues.push(issue("ig_stimulus_kind_invalid", at("kind"), `"${kind}" is not a stimulus kind. Use text, audio, video, image or mixed.`));
|
|
1173
|
-
if (isRecord$
|
|
1411
|
+
if (isRecord$6(stimulus.media)) issues.push(...checkMedia(stimulus.media, at("media")));
|
|
1174
1412
|
return issues;
|
|
1175
1413
|
}
|
|
1176
1414
|
const hasBody = typeof stimulus.body === "string" && stimulus.body.trim() !== "";
|
|
1177
1415
|
const htmlPresent = !isUnset(stimulus.bodyHtml);
|
|
1178
1416
|
if (!hasBody && (kind === "text" || kind === "mixed" || htmlPresent)) issues.push(issue("ig_stimulus_body_required", at("body"), isUnwritten(stimulus.bodyHtml) ? "Write the passage." : "Write the passage as plain text too. The rich-text version is shown only where a sanitiser is supplied."));
|
|
1179
1417
|
if (kind !== "text" && isUnset(stimulus.media)) issues.push(issue("ig_stimulus_media_required", at("media"), "Add the recording, video or image."));
|
|
1180
|
-
else if (isRecord$
|
|
1418
|
+
else if (isRecord$6(stimulus.media)) {
|
|
1181
1419
|
const allowed = MEDIA_FOR_KIND[kind];
|
|
1182
1420
|
const mediaType = stimulus.media.type;
|
|
1183
1421
|
if (allowed !== void 0 && typeof mediaType === "string" && !allowed.includes(mediaType)) issues.push(issue("ig_stimulus_media_kind", at("media", "type"), `A "${kind}" stimulus cannot carry ${mediaType} media. Use ${allowed.join(" or ")}.`));
|
|
@@ -1204,7 +1442,7 @@ function checkItems(draft) {
|
|
|
1204
1442
|
const seen = /* @__PURE__ */ new Set();
|
|
1205
1443
|
const repeated = /* @__PURE__ */ new Set();
|
|
1206
1444
|
for (const [index, item] of items.entries()) {
|
|
1207
|
-
if (!isRecord$
|
|
1445
|
+
if (!isRecord$6(item)) continue;
|
|
1208
1446
|
const ordinal = index + 1;
|
|
1209
1447
|
if (isMissingId(item.id)) issues.push(issue("ig_item_id_required", [
|
|
1210
1448
|
"items",
|
|
@@ -1301,7 +1539,7 @@ function validateDraft(type, draft) {
|
|
|
1301
1539
|
function validateDraftInScope(type, draft) {
|
|
1302
1540
|
const descriptor = getActivityTypeDescriptor(type);
|
|
1303
1541
|
if (descriptor === void 0) throw new UnknownActivityTypeError(String(type));
|
|
1304
|
-
const reported = isRecord$
|
|
1542
|
+
const reported = isRecord$6(draft) && descriptor.authoring?.checkDraft !== void 0 ? descriptor.authoring.checkDraft(draft).map(normalise) : [];
|
|
1305
1543
|
const issues = [...reported];
|
|
1306
1544
|
const parsed = readSchema(descriptor.schema, draft, `Activity type "${descriptor.type}"`, { reportInput: true });
|
|
1307
1545
|
if (!parsed.success) {
|
|
@@ -1410,9 +1648,9 @@ function normalise(found) {
|
|
|
1410
1648
|
function critiqueDraft(type, draft) {
|
|
1411
1649
|
const descriptor = getActivityTypeDescriptor(type);
|
|
1412
1650
|
if (descriptor === void 0) throw new UnknownActivityTypeError(String(type));
|
|
1651
|
+
if (!isRecord$6(draft)) return [];
|
|
1413
1652
|
const critique = descriptor.authoring?.critique;
|
|
1414
|
-
|
|
1415
|
-
return withValidationScope(() => critique(draft)).map(normaliseFinding);
|
|
1653
|
+
return withValidationScope(() => [...critique === void 0 ? [] : critique(draft), ...recordingFindings(draft)]).map(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",
|
|
@@ -2929,6 +3171,6 @@ function checkAiWritingFeedback(raw, request) {
|
|
|
2929
3171
|
};
|
|
2930
3172
|
}
|
|
2931
3173
|
//#endregion
|
|
2932
|
-
export {
|
|
3174
|
+
export { AI_COACHING_MAX_TIP_LENGTH as A, aiHintRequest as B, createItemGroupDraft as C, listMediaRefs as D, validateItemGroupDraft as E, AI_HINT_TYPES as F, hintRevealsAnswer as G, buildAiFacts as H, AI_TEXT_MAX_LENGTH as I, readAiTextResult as K, aiAllowedByContent as L, aiCoachingRequest as M, checkAiCoaching as N, mapMediaRefs as O, AI_EXPLANATION_TYPES as P, aiExplanationRequest as R, createInteractiveVideoDraft as S, critiqueItemGroupDraft as T, checkAiExplanation as U, aiSupports as V, checkAiHint as W, aiCritiqueRequest as _, checkAiGrading as a, critiqueDraft as b, AI_DRAFTS_MAX_COUNT as c, aiDraftsRequest as d, checkAiDrafts as f, AI_CRITIQUE_MAX_FINDINGS as g, AI_CRITIQUE_MAX_FIELD_LENGTH as h, aiGradingRequest as i, AI_COACHING_MAX_WORDS as j, transcriptOf as k, AI_DRAFTS_MAX_REPAIRS as l, interactiveVideoFromDrafts as m, checkAiWritingFeedback as n, AI_WRITING_MAX_CORRECTIONS as o, generateDrafts as p, AI_MATERIAL_MAX_LENGTH as q, AI_GRADING_PROBABILITY_TOLERANCE as r, AI_WRITING_MAX_FIELD_LENGTH as s, aiWritingFeedbackRequest as t, aiDraftsRepairRequest as u, checkAiCritique as v, critiqueDrafts as w, validateDraft as x, createDraft as y, aiGradeOf as z };
|
|
2933
3175
|
|
|
2934
|
-
//# sourceMappingURL=ai-writing-
|
|
3176
|
+
//# sourceMappingURL=ai-writing-OJ5m_1EB.js.map
|