From 023d00c65182b1bdf874cd38d8af7eb7453f0925 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 17 Jun 2026 11:23:12 +0200 Subject: [PATCH] bisaya-kurs korrigiert --- ...air-invalid-srs-fragment-sentence-pairs.js | 89 +++++++++++++++---- backend/services/vocabService.js | 40 +++++++-- 2 files changed, 107 insertions(+), 22 deletions(-) diff --git a/backend/scripts/repair-invalid-srs-fragment-sentence-pairs.js b/backend/scripts/repair-invalid-srs-fragment-sentence-pairs.js index f389e46..83fbd4c 100644 --- a/backend/scripts/repair-invalid-srs-fragment-sentence-pairs.js +++ b/backend/scripts/repair-invalid-srs-fragment-sentence-pairs.js @@ -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) => { diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index 0045925..507968f 100644 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -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;