feat(bisaya-course): implement core pattern extraction and merging for exercises
All checks were successful
Deploy to production / deploy (push) Successful in 2m55s
All checks were successful
Deploy to production / deploy (push) Successful in 2m55s
- Added functions to derive core patterns from various exercise types, including gap-fill, transformation, and multiple-choice. - Implemented logic to merge derived core patterns with existing lesson patterns, ensuring a minimum count for effective lesson content. - Enhanced the `createBisayaCourseContent` function to update lessons with merged core patterns and provide detailed logging based on the VOCAB_STRICT_AUDIT environment variable.
This commit is contained in:
@@ -85,6 +85,127 @@ function buildCorePatternGlossLookup(didactics) {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function phraseWordCount(value) {
|
||||||
|
return countWords(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPhraseLike(value) {
|
||||||
|
return phraseWordCount(value) >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeQuotedPrompt(value) {
|
||||||
|
return normalizeText(String(value || '').replace(/[„“"']/g, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstQuotedContent(value) {
|
||||||
|
const text = String(value || '');
|
||||||
|
const doubleQuote = text.match(/"([^"]+)"/);
|
||||||
|
if (doubleQuote?.[1]) return normalizeText(doubleQuote[1]);
|
||||||
|
const deQuote = text.match(/„([^“]+)“/);
|
||||||
|
if (deQuote?.[1]) return normalizeText(deQuote[1]);
|
||||||
|
const singleQuote = text.match(/'([^']+)'/);
|
||||||
|
if (singleQuote?.[1]) return normalizeText(singleQuote[1]);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUniquePattern(out, seen, target, gloss) {
|
||||||
|
const t = normalizeText(target);
|
||||||
|
const g = normalizeText(gloss);
|
||||||
|
if (!t || !g) return;
|
||||||
|
if (!isPhraseLike(t)) return;
|
||||||
|
const key = `${t.toLowerCase()}|${g.toLowerCase()}`;
|
||||||
|
if (seen.has(key)) return;
|
||||||
|
seen.add(key);
|
||||||
|
out.push({ target: t, gloss: g });
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPairsFromGapFill(questionData, answerData, out, seen) {
|
||||||
|
const text = String(questionData?.text || '');
|
||||||
|
const answers = Array.isArray(answerData?.answers) ? answerData.answers : [];
|
||||||
|
if (!text || answers.length === 0) return;
|
||||||
|
|
||||||
|
const hints = [];
|
||||||
|
const hintRegex = /\(([^)]+)\)/g;
|
||||||
|
let match = hintRegex.exec(text);
|
||||||
|
while (match) {
|
||||||
|
hints.push(normalizeText(match[1]));
|
||||||
|
match = hintRegex.exec(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(answers.length, hints.length); i += 1) {
|
||||||
|
addUniquePattern(out, seen, answers[i], hints[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPairsFromTransformation(questionData, answerData, out, seen) {
|
||||||
|
const src = normalizeText(questionData?.text || '');
|
||||||
|
const trg = normalizeText(answerData?.correct || '');
|
||||||
|
if (!src || !trg) return;
|
||||||
|
const sourceLanguage = normalizeText(questionData?.sourceLanguage || '').toLowerCase();
|
||||||
|
const targetLanguage = normalizeText(questionData?.targetLanguage || '').toLowerCase();
|
||||||
|
if (sourceLanguage === 'deutsch' || targetLanguage === 'bisaya') {
|
||||||
|
addUniquePattern(out, seen, trg, src);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addUniquePattern(out, seen, src, trg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPairsFromMultipleChoice(questionData, answerData, out, seen) {
|
||||||
|
const question = normalizeText(questionData?.question || '');
|
||||||
|
const options = Array.isArray(questionData?.options) ? questionData.options : [];
|
||||||
|
const index = Number(answerData?.correctAnswer);
|
||||||
|
const correct = normalizeText(options[index] || '');
|
||||||
|
if (!question || !correct) return;
|
||||||
|
|
||||||
|
const quoted = firstQuotedContent(question);
|
||||||
|
const lower = question.toLowerCase();
|
||||||
|
if (!quoted) return;
|
||||||
|
|
||||||
|
if (lower.startsWith('was bedeutet')) {
|
||||||
|
addUniquePattern(out, seen, quoted, correct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lower.startsWith('wie sagt man')) {
|
||||||
|
addUniquePattern(out, seen, correct, normalizeQuotedPrompt(quoted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveLessonCorePatternsFromExercises(exercises) {
|
||||||
|
const out = [];
|
||||||
|
const seen = new Set();
|
||||||
|
(Array.isArray(exercises) ? exercises : []).forEach((exercise) => {
|
||||||
|
const questionData = exercise?.questionData || {};
|
||||||
|
const answerData = exercise?.answerData || {};
|
||||||
|
const type = String(questionData?.type || '');
|
||||||
|
if (type === 'gap_fill') {
|
||||||
|
extractPairsFromGapFill(questionData, answerData, out, seen);
|
||||||
|
} else if (type === 'transformation') {
|
||||||
|
extractPairsFromTransformation(questionData, answerData, out, seen);
|
||||||
|
} else if (type === 'multiple_choice') {
|
||||||
|
extractPairsFromMultipleChoice(questionData, answerData, out, seen);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeCorePatternsForLesson(didactics, exerciseDerived, minCount = 8) {
|
||||||
|
const out = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const basePatterns = Array.isArray(didactics?.corePatterns) ? didactics.corePatterns : [];
|
||||||
|
|
||||||
|
basePatterns.forEach((entry) => {
|
||||||
|
const normalized = normalizeCorePatternEntry(entry);
|
||||||
|
if (!normalized?.target || !normalized?.gloss) return;
|
||||||
|
addUniquePattern(out, seen, normalized.target, normalized.gloss);
|
||||||
|
});
|
||||||
|
exerciseDerived.forEach((entry) => {
|
||||||
|
addUniquePattern(out, seen, entry?.target, entry?.gloss);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (out.length >= minCount) return out;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeGapFillHintText(lessonTitle, text, answers, glossLookup) {
|
function sanitizeGapFillHintText(lessonTitle, text, answers, glossLookup) {
|
||||||
const source = String(text || '');
|
const source = String(text || '');
|
||||||
const normalizedAnswers = Array.isArray(answers)
|
const normalizedAnswers = Array.isArray(answers)
|
||||||
@@ -4454,6 +4575,18 @@ async function createBisayaCourseContent() {
|
|||||||
|
|
||||||
// Erstelle Übungen
|
// Erstelle Übungen
|
||||||
const lessonDidactics = getLessonDidactics(lesson);
|
const lessonDidactics = getLessonDidactics(lesson);
|
||||||
|
const derivedCorePatterns = deriveLessonCorePatternsFromExercises(exercises);
|
||||||
|
const mergedCorePatterns = mergeCorePatternsForLesson(lessonDidactics, derivedCorePatterns, 8);
|
||||||
|
if (mergedCorePatterns.length >= 8) {
|
||||||
|
await lesson.update({ corePatterns: mergedCorePatterns });
|
||||||
|
if (process.env.VOCAB_STRICT_AUDIT === '1') {
|
||||||
|
console.log(` ✅ [${lesson.title}] corePatterns auf ${mergedCorePatterns.length} Satzphrasen aktualisiert`);
|
||||||
|
}
|
||||||
|
} else if (process.env.VOCAB_STRICT_AUDIT === '1') {
|
||||||
|
console.warn(
|
||||||
|
` ⚠️ [${lesson.title}] Nur ${mergedCorePatterns.length} Satzphrasen mit sicherer Gloss gefunden (Ziel: 8)`
|
||||||
|
);
|
||||||
|
}
|
||||||
let exerciseNumber = 1;
|
let exerciseNumber = 1;
|
||||||
for (const exerciseData of exercises) {
|
for (const exerciseData of exercises) {
|
||||||
const { exercise, fixes, warnings } = sanitizeExerciseForConsistency(
|
const { exercise, fixes, warnings } = sanitizeExerciseForConsistency(
|
||||||
|
|||||||
Reference in New Issue
Block a user