feat(VocabService): improve SRS pair validation and introduce new text analysis methods
All checks were successful
Deploy to production / deploy (push) Successful in 2m13s

- Refactored SRS pair validation logic by introducing the `_isTrainableSrsPair` method to enhance the criteria for acceptable learning and reference pairs.
- Added `_isInstructionLikeText` method to filter out instructional texts from SRS pairs, improving the quality of vocabulary training.
- Updated various methods to utilize the new validation logic, ensuring consistent handling of vocabulary entries across the service.
- Enhanced the `_courseHasDueSrsItems` and `_extractTrainerVocabsFromLessonDidactics` methods to improve data retrieval and filtering based on the new criteria.
This commit is contained in:
Torsten Schulz (local)
2026-04-20 08:21:18 +02:00
parent b6d749f781
commit e6c90c219b

View File

@@ -42,7 +42,7 @@ export default class VocabService {
.map((entry) => { .map((entry) => {
const learning = String(entry?.learning || '').trim(); const learning = String(entry?.learning || '').trim();
const reference = String(entry?.reference || '').trim(); const reference = String(entry?.reference || '').trim();
if (!learning || !reference || this._normalizeSrsText(learning) === this._normalizeSrsText(reference)) { if (!this._isTrainableSrsPair({ learning, reference })) {
return null; return null;
} }
const direction = String(entry?.direction || 'BOTH').toUpperCase(); const direction = String(entry?.direction || 'BOTH').toUpperCase();
@@ -61,6 +61,30 @@ export default class VocabService {
.filter(Boolean); .filter(Boolean);
} }
_isInstructionLikeText(value) {
const text = String(value || '').trim();
if (!text) {
return false;
}
const wordCount = text.split(/\s+/).filter(Boolean).length;
if (wordCount < 3) {
return false;
}
return /^(sage|sag|frage|frag|bitte|stelle|sprich|erzähle|beschreibe|bilde|wähle|ordne|übersetze|nenne|nenn|beginne|verwende|reagiere|kombiniere|spiele|löse|beantworte|ergänze|formuliere)\b/i.test(text);
}
_isTrainableSrsPair(entry) {
const learning = String(entry?.learning || '').trim();
const reference = String(entry?.reference || '').trim();
if (!learning || !reference || this._normalizeSrsText(learning) === this._normalizeSrsText(reference)) {
return false;
}
return !this._isInstructionLikeText(learning) && !this._isInstructionLikeText(reference);
}
_calculateSrsSchedule(item, { correct, rating = null } = {}) { _calculateSrsSchedule(item, { correct, rating = null } = {}) {
const now = new Date(); const now = new Date();
const previousStage = Math.max(0, Number(item?.stage) || 0); const previousStage = Math.max(0, Number(item?.stage) || 0);
@@ -459,16 +483,17 @@ export default class VocabService {
} }
async _courseHasDueSrsItems(userId, courseId) { async _courseHasDueSrsItems(userId, courseId) {
const due = await VocabSrsItem.count({ const dueItems = await VocabSrsItem.findAll({
where: { where: {
userId, userId,
courseId: Number(courseId), courseId: Number(courseId),
nextDueAt: { nextDueAt: {
[Op.lte]: new Date() [Op.lte]: new Date()
} }
} },
attributes: ['learning', 'reference']
}); });
return due > 0; return dueItems.some((item) => this._isTrainableSrsPair(item));
} }
async _getUserByHashedId(hashedUserId) { async _getUserByHashedId(hashedUserId) {
@@ -713,7 +738,7 @@ export default class VocabService {
const question = String(qData.question || qData.text || ''); const question = String(qData.question || qData.text || '');
let match = question.match(/Wie sagt man ['"]([^'"]+)['"]/i); let match = question.match(/Wie sagt man ['"]([^'"]+)['"]/i);
if (match && match[1] && correctAnswer && match[1].trim() !== String(correctAnswer).trim()) { if (match && this._isTrainableSrsPair({ learning: match[1], reference: String(correctAnswer) })) {
vocabMap.set(`${match[1]}-${correctAnswer}`, { vocabMap.set(`${match[1]}-${correctAnswer}`, {
learning: match[1], learning: match[1],
reference: String(correctAnswer) reference: String(correctAnswer)
@@ -722,7 +747,7 @@ export default class VocabService {
} }
match = question.match(/Was bedeutet ['"]([^'"]+)['"]/i); match = question.match(/Was bedeutet ['"]([^'"]+)['"]/i);
if (match && match[1] && correctAnswer && match[1].trim() !== String(correctAnswer).trim()) { if (match && this._isTrainableSrsPair({ learning: String(correctAnswer), reference: match[1] })) {
vocabMap.set(`${correctAnswer}-${match[1]}`, { vocabMap.set(`${correctAnswer}-${match[1]}`, {
learning: String(correctAnswer), learning: String(correctAnswer),
reference: match[1] reference: match[1]
@@ -745,7 +770,7 @@ export default class VocabService {
answers.forEach((answer, index) => { answers.forEach((answer, index) => {
const nativeWord = nativeWords[index]; const nativeWord = nativeWords[index];
const normalizedAnswer = String(answer || '').trim(); const normalizedAnswer = String(answer || '').trim();
if (!nativeWord || !normalizedAnswer || nativeWord === normalizedAnswer) { if (!this._isTrainableSrsPair({ learning: nativeWord, reference: normalizedAnswer })) {
return; return;
} }
vocabMap.set(`${nativeWord}-${normalizedAnswer}`, { vocabMap.set(`${nativeWord}-${normalizedAnswer}`, {
@@ -764,31 +789,13 @@ export default class VocabService {
_extractTrainerVocabsFromLessonDidactics(lesson) { _extractTrainerVocabsFromLessonDidactics(lesson) {
const vocabMap = new Map(); const vocabMap = new Map();
const speakingPrompts = Array.isArray(lesson?.speakingPrompts) ? lesson.speakingPrompts : [];
const practicalTasks = Array.isArray(lesson?.practicalTasks) ? lesson.practicalTasks : [];
const corePatterns = Array.isArray(lesson?.corePatterns) ? lesson.corePatterns : []; const corePatterns = Array.isArray(lesson?.corePatterns) ? lesson.corePatterns : [];
corePatterns.forEach((entry) => { corePatterns.forEach((entry) => {
const pattern = this._normalizeCorePatternEntry(entry); const pattern = this._normalizeCorePatternEntry(entry);
const reference = String(pattern?.target || '').trim(); const reference = String(pattern?.target || '').trim();
const learning = String(pattern?.gloss || '').trim(); const learning = String(pattern?.gloss || '').trim();
if (!learning || !reference || learning === reference) return; if (!this._isTrainableSrsPair({ learning, reference })) return;
vocabMap.set(`${learning}-${reference}`, { learning, reference });
});
speakingPrompts.forEach((prompt, index) => {
const learning = String(prompt?.prompt || prompt?.title || '').trim();
const refEntry = corePatterns[index] ?? corePatterns[0];
const reference = String(prompt?.cue || this._corePatternTarget(refEntry) || '').trim();
if (!learning || !reference || learning === reference) return;
vocabMap.set(`${learning}-${reference}`, { learning, reference });
});
practicalTasks.forEach((task, index) => {
const learning = String(task?.text || task?.title || '').trim();
const refEntry = corePatterns[index] ?? corePatterns[0];
const reference = String(this._corePatternTarget(refEntry) || '').trim();
if (!learning || !reference || learning === reference) return;
vocabMap.set(`${learning}-${reference}`, { learning, reference }); vocabMap.set(`${learning}-${reference}`, { learning, reference });
}); });
@@ -1808,16 +1815,17 @@ export default class VocabService {
[Op.lte]: now [Op.lte]: now
} }
}; };
const totalDueCount = await VocabSrsItem.count({ where: dueWhere }); const dueRows = await VocabSrsItem.findAll({
const rows = await VocabSrsItem.findAll({
where: dueWhere, where: dueWhere,
order: [ order: [
['nextDueAt', 'ASC'], ['nextDueAt', 'ASC'],
['wrongCount', 'DESC'], ['wrongCount', 'DESC'],
['stage', 'ASC'] ['stage', 'ASC']
], ]
limit
}); });
const validDueRows = dueRows.filter((item) => this._isTrainableSrsPair(item));
const rows = validDueRows.slice(0, limit);
const totalDueCount = validDueRows.length;
return { return {
courseId: course.id, courseId: course.id,