bisaya-kurs korrigiert
All checks were successful
Deploy to production / deploy (push) Successful in 2m13s
All checks were successful
Deploy to production / deploy (push) Successful in 2m13s
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import { sequelize } from '../utils/sequelize.js';
|
import { sequelize } from '../utils/sequelize.js';
|
||||||
import VocabSrsItem from '../models/community/vocab_srs_item.js';
|
import VocabSrsItem from '../models/community/vocab_srs_item.js';
|
||||||
|
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
return {
|
return {
|
||||||
@@ -76,30 +77,84 @@ function isStaleInstructionCard(left, right) {
|
|||||||
return isInstructionLikeText(left) || isInstructionLikeText(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() {
|
async function main() {
|
||||||
const { apply } = parseArgs(process.argv.slice(2));
|
const { apply } = parseArgs(process.argv.slice(2));
|
||||||
|
const staleLegacyGapFillPairs = await collectStaleLegacyGapFillPairs();
|
||||||
const items = await VocabSrsItem.findAll({
|
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']],
|
order: [['id', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
const matches = items.filter((item) =>
|
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) => {
|
matches.forEach((item) => {
|
||||||
|
|||||||
@@ -840,15 +840,14 @@ export default class VocabService {
|
|||||||
const answers = Array.isArray(aData.answers)
|
const answers = Array.isArray(aData.answers)
|
||||||
? aData.answers
|
? aData.answers
|
||||||
: (aData.correct ? (Array.isArray(aData.correct) ? aData.correct : [aData.correct]) : []);
|
: (aData.correct ? (Array.isArray(aData.correct) ? aData.correct : [aData.correct]) : []);
|
||||||
const text = String(qData.text || '');
|
const hintPairs = this._extractGapFillHintPairs(String(qData.text || ''), answers.length);
|
||||||
const nativeWords = Array.from(text.matchAll(/\(([^)]+)\)/g), (m) => String(m[1] || '').trim());
|
|
||||||
|
|
||||||
if (!answers.length || !nativeWords.length) {
|
if (!answers.length || !hintPairs.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
answers.forEach((answer, index) => {
|
answers.forEach((answer, index) => {
|
||||||
const nativeWord = nativeWords[index];
|
const nativeWord = hintPairs[index] || '';
|
||||||
const normalizedAnswer = String(answer || '').trim();
|
const normalizedAnswer = String(answer || '').trim();
|
||||||
if (!this._isTrainableSrsPair({ learning: nativeWord, reference: normalizedAnswer })) {
|
if (!this._isTrainableSrsPair({ learning: nativeWord, reference: normalizedAnswer })) {
|
||||||
return;
|
return;
|
||||||
@@ -867,6 +866,28 @@ export default class VocabService {
|
|||||||
return Array.from(vocabMap.values());
|
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) {
|
_extractTrainerVocabsFromLessonDidactics(lesson) {
|
||||||
const vocabMap = new Map();
|
const vocabMap = new Map();
|
||||||
const corePatterns = Array.isArray(lesson?.corePatterns) ? lesson.corePatterns : [];
|
const corePatterns = Array.isArray(lesson?.corePatterns) ? lesson.corePatterns : [];
|
||||||
@@ -1913,7 +1934,16 @@ export default class VocabService {
|
|||||||
['stage', 'ASC']
|
['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 rows = validDueRows.slice(0, limit);
|
||||||
const totalDueCount = validDueRows.length;
|
const totalDueCount = validDueRows.length;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user