bisaya-kurs korrigiert
All checks were successful
Deploy to production / deploy (push) Successful in 2m13s

This commit is contained in:
Torsten Schulz (local)
2026-06-17 11:23:12 +02:00
parent fc4b607ac2
commit 023d00c651
2 changed files with 107 additions and 22 deletions

View File

@@ -12,6 +12,7 @@
import { Op } from 'sequelize';
import { sequelize } from '../utils/sequelize.js';
import VocabSrsItem from '../models/community/vocab_srs_item.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
function parseArgs(argv) {
return {
@@ -76,30 +77,84 @@ function isStaleInstructionCard(left, right) {
return isInstructionLikeText(left) || isInstructionLikeText(right);
}
function normalizePairSide(value) {
return String(value || '')
.trim()
.toLowerCase()
.normalize('NFKC')
.replace(/[\p{P}\p{S}]+/gu, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function pairSignature(left, right) {
return `${normalizePairSide(left)}|${normalizePairSide(right)}`;
}
function extractLegacyGapFillHints(text) {
return Array.from(String(text || '').matchAll(/\(([^)]+)\)/g), (m) => String(m[1] || '').trim()).filter(Boolean);
}
function extractFixedGapFillHints(text, expectedCount = 0) {
const source = String(text || '');
const gapRegex = /\{\s*gap\s*\}/gi;
const gapMatches = Array.from(source.matchAll(gapRegex));
const hints = gapMatches.map((match, index) => {
const start = match.index + match[0].length;
const nextStart = index + 1 < gapMatches.length ? gapMatches[index + 1].index : source.length;
const segment = source.slice(start, nextStart);
const hintMatch = segment.match(/\(([^)]+)\)/);
return String(hintMatch?.[1] || '').trim();
}).filter(Boolean);
return expectedCount > 0 ? hints.slice(0, expectedCount) : hints;
}
async function collectStaleLegacyGapFillPairs() {
const exercises = await VocabGrammarExercise.findAll({
attributes: ['questionData', 'answerData'],
});
const stalePairs = new Set();
exercises.forEach((exercise) => {
const qData = exercise.questionData || {};
const aData = exercise.answerData || {};
const exerciseType = qData.type || '';
if (exerciseType !== 'gap_fill') return;
const answers = Array.isArray(aData.answers)
? aData.answers
: (aData.correct ? (Array.isArray(aData.correct) ? aData.correct : [aData.correct]) : []);
if (!answers.length) return;
const text = String(qData.text || '');
const legacyHints = extractLegacyGapFillHints(text);
const fixedHints = extractFixedGapFillHints(text, answers.length);
const fixedSignatures = new Set(
fixedHints.map((hint, index) => pairSignature(hint, answers[index]))
);
legacyHints.slice(0, answers.length).forEach((hint, index) => {
const signature = pairSignature(hint, answers[index]);
if (!fixedSignatures.has(signature)) {
stalePairs.add(signature);
}
});
});
return stalePairs;
}
async function main() {
const { apply } = parseArgs(process.argv.slice(2));
const staleLegacyGapFillPairs = await collectStaleLegacyGapFillPairs();
const items = await VocabSrsItem.findAll({
where: {
[Op.or]: [
{ learning: { [Op.like]: '%/%' } },
{ reference: { [Op.like]: '%/%' } },
{ learning: { [Op.like]: '%?%' } },
{ reference: { [Op.like]: '%?%' } },
{ learning: { [Op.iLike]: 'Begrüße %' } },
{ reference: { [Op.iLike]: 'Begrüße %' } },
{ learning: { [Op.iLike]: 'Begruesse %' } },
{ reference: { [Op.iLike]: 'Begruesse %' } },
{ learning: { [Op.iLike]: 'Drücke %' } },
{ reference: { [Op.iLike]: 'Drücke %' } },
{ learning: { [Op.iLike]: 'Druecke %' } },
{ reference: { [Op.iLike]: 'Druecke %' } },
],
},
order: [['id', 'ASC']],
});
const matches = items.filter((item) =>
isStaleQuestionCueCard(item.learning, item.reference) || isStaleInstructionCard(item.learning, item.reference)
isStaleQuestionCueCard(item.learning, item.reference)
|| isStaleInstructionCard(item.learning, item.reference)
|| staleLegacyGapFillPairs.has(pairSignature(item.learning, item.reference))
);
matches.forEach((item) => {

View File

@@ -840,15 +840,14 @@ export default class VocabService {
const answers = Array.isArray(aData.answers)
? aData.answers
: (aData.correct ? (Array.isArray(aData.correct) ? aData.correct : [aData.correct]) : []);
const text = String(qData.text || '');
const nativeWords = Array.from(text.matchAll(/\(([^)]+)\)/g), (m) => String(m[1] || '').trim());
const hintPairs = this._extractGapFillHintPairs(String(qData.text || ''), answers.length);
if (!answers.length || !nativeWords.length) {
if (!answers.length || !hintPairs.length) {
return;
}
answers.forEach((answer, index) => {
const nativeWord = nativeWords[index];
const nativeWord = hintPairs[index] || '';
const normalizedAnswer = String(answer || '').trim();
if (!this._isTrainableSrsPair({ learning: nativeWord, reference: normalizedAnswer })) {
return;
@@ -867,6 +866,28 @@ export default class VocabService {
return Array.from(vocabMap.values());
}
_extractGapFillHintPairs(text, expectedCount = 0) {
const source = String(text || '');
if (!source) return [];
const gapRegex = /\{\s*gap\s*\}/gi;
const gapMatches = Array.from(source.matchAll(gapRegex));
if (!gapMatches.length) return [];
const hints = gapMatches.map((match, index) => {
const start = match.index + match[0].length;
const nextStart = index + 1 < gapMatches.length ? gapMatches[index + 1].index : source.length;
const segment = source.slice(start, nextStart);
const hintMatch = segment.match(/\(([^)]+)\)/);
return String(hintMatch?.[1] || '').trim();
}).filter(Boolean);
if (expectedCount > 0) {
return hints.slice(0, expectedCount);
}
return hints;
}
_extractTrainerVocabsFromLessonDidactics(lesson) {
const vocabMap = new Map();
const corePatterns = Array.isArray(lesson?.corePatterns) ? lesson.corePatterns : [];
@@ -1913,7 +1934,16 @@ export default class VocabService {
['stage', 'ASC']
]
});
const validDueRows = dueRows.filter((item) => this._isTrainableSrsPair(item));
const validPool = await this.getCompletedLessonVocabPool(hashedUserId, course.id);
const validKeys = new Set(
(Array.isArray(validPool?.vocabs) ? validPool.vocabs : [])
.map((entry) => String(entry?.itemKey || '').trim())
.filter(Boolean)
);
const validDueRows = dueRows.filter((item) =>
this._isTrainableSrsPair(item)
&& (!validKeys.size || validKeys.has(String(item.itemKey || '').trim()))
);
const rows = validDueRows.slice(0, limit);
const totalDueCount = validDueRows.length;