#!/usr/bin/env node /** * Script zum Erstellen von Übungen für Deutschkurse aus Sicht von Bisaya-Lernenden. * * Verwendung: * node backend/scripts/create-german-for-bisaya-course-content.js */ import { sequelize } from '../utils/sequelize.js'; import VocabCourseLesson from '../models/community/vocab_course_lesson.js'; import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js'; import User from '../models/community/user.js'; import { GERMAN_FOR_BISAYA_PHASE1_DIDACTICS } from './german-for-bisaya-phase1.js'; import { GERMAN_FOR_BISAYA_PHASE3_DIDACTICS } from './german-for-bisaya-phase3-extension.js'; import { GERMAN_FOR_BISAYA_PHASE4_DIDACTICS } from './german-for-bisaya-phase4-extension.js'; import { GERMAN_FOR_BISAYA_PHASE5_DIDACTICS } from './german-for-bisaya-phase5-extension.js'; function withTypeName(exerciseTypeName, exercise) { return { ...exercise, exerciseTypeName }; } const GERMAN_DIDACTICS = { ...GERMAN_FOR_BISAYA_PHASE1_DIDACTICS, ...GERMAN_FOR_BISAYA_PHASE3_DIDACTICS, ...GERMAN_FOR_BISAYA_PHASE4_DIDACTICS, ...GERMAN_FOR_BISAYA_PHASE5_DIDACTICS }; const GENERIC_DISTRACTOR_PATTERNS = Array.from(new Set( Object.values(GERMAN_DIDACTICS) .flatMap((entry) => Array.isArray(entry?.corePatterns) ? entry.corePatterns : []) .map((pattern) => String(pattern || '').trim()) .filter(Boolean) )).slice(0, 300); function normalizeText(value) { return String(value || '') .trim() .replace(/\s+/g, ' '); } function simpleHash(value) { return Array.from(String(value || '')).reduce((sum, char) => sum + char.charCodeAt(0), 0); } function rotateArray(values, offset) { if (!Array.isArray(values) || values.length === 0) return []; const normalizedOffset = ((offset % values.length) + values.length) % values.length; return values.slice(normalizedOffset).concat(values.slice(0, normalizedOffset)); } function getLessonDidactics(lesson) { const staticDidactics = GERMAN_DIDACTICS[lesson.title] || {}; return { learningGoals: Array.isArray(lesson.learningGoals) ? lesson.learningGoals : (staticDidactics.learningGoals || []), corePatterns: (Array.isArray(lesson.corePatterns) ? lesson.corePatterns : (staticDidactics.corePatterns || [])) .map((entry) => normalizeText(entry)) .filter(Boolean), grammarFocus: Array.isArray(lesson.grammarFocus) ? lesson.grammarFocus : (staticDidactics.grammarFocus || []), speakingPrompts: Array.isArray(lesson.speakingPrompts) ? lesson.speakingPrompts : (staticDidactics.speakingPrompts || []), practicalTasks: Array.isArray(lesson.practicalTasks) ? lesson.practicalTasks : (staticDidactics.practicalTasks || []) }; } function getScenarioPrompt(lesson, didactics) { const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null; const practicalTask = Array.isArray(didactics.practicalTasks) ? didactics.practicalTasks[0] : null; if (speakingPrompt?.prompt) return speakingPrompt.prompt; if (practicalTask?.text) return practicalTask.text; if (lesson.description) return lesson.description; return `Reagiere passend in einer Alltagssituation aus der Lektion "${lesson.title}".`; } function getChoiceQuestion(lesson, didactics) { const scenarioPrompt = getScenarioPrompt(lesson, didactics); switch (lesson.lessonType) { case 'conversation': return `${scenarioPrompt} Welche deutsche Formulierung passt am besten?`; case 'grammar': return `Welche deutsche Struktur passt am besten zum Schwerpunkt "${lesson.title}"?`; case 'review': return `Welche Formulierung solltest du aus "${lesson.title}" sicher wiedererkennen?`; case 'culture': return `Welche Formulierung passt besonders gut zum kulturellen Schwerpunkt "${lesson.title}"?`; case 'vocab': default: return `Welche Formulierung gehört thematisch zu "${lesson.title}"?`; } } function pickDistractors(pattern, allPatterns, count) { return allPatterns .filter((entry) => entry !== pattern) .slice(0, count); } function buildChoiceExercise(lesson, didactics, pattern, allPatterns, variant = 0) { const distractors = pickDistractors(pattern, allPatterns, 3); if (distractors.length < 3) return null; const options = rotateArray([pattern, ...distractors], simpleHash(`${lesson.title}:${pattern}:${variant}`) % 4); return { exerciseTypeId: 2, title: `${lesson.title}: Passende Formulierung wählen`, instruction: 'Wähle die natürlichste deutsche Formulierung für die Situation oder den Schwerpunkt der Lektion.', questionData: { type: 'multiple_choice', question: getChoiceQuestion(lesson, didactics), options }, answerData: { type: 'multiple_choice', correctAnswer: options.indexOf(pattern) }, explanation: `"${pattern}" ist ein zentrales deutsches Muster dieser Lektion.` }; } function pickGapTarget(pattern) { const tokens = normalizeText(pattern) .split(' ') .map((token) => token.trim()) .filter(Boolean); const candidates = tokens .map((token, index) => ({ token, index, score: token.replace(/[.,?!]/g, '').length })) .filter(({ token }) => token.replace(/[.,?!]/g, '').length >= 3); if (candidates.length === 0) return null; candidates.sort((left, right) => right.score - left.score); return candidates[0]; } function buildGapExercise(lessonTitle, pattern) { const gapTarget = pickGapTarget(pattern); if (!gapTarget) return null; const tokens = normalizeText(pattern).split(' '); tokens[gapTarget.index] = '{gap}'; return { exerciseTypeId: 1, title: `${lessonTitle}: Muster vervollständigen`, instruction: 'Fülle die Lücke mit dem passenden deutschen Ausdruck.', questionData: { type: 'gap_fill', text: tokens.join(' '), gaps: 1 }, answerData: { type: 'gap_fill', answers: [gapTarget.token.replace(/[.,?!]/g, '')] }, explanation: `Das vollständige deutsche Kernmuster lautet: "${pattern}".` }; } function buildSentenceExercise(lessonTitle, pattern) { const tokens = normalizeText(pattern) .replace(/[?!]/g, '') .split(' ') .filter(Boolean); if (tokens.length < 2) return null; return { exerciseTypeId: 3, title: `${lessonTitle}: Satz bauen`, instruction: 'Ordne die Wörter zu einem korrekten deutschen Satz.', questionData: { type: 'sentence_building', question: `Baue das Kernmuster aus der Lektion "${lessonTitle}".`, tokens }, answerData: { correct: [normalizeText(pattern)] }, explanation: `Dieses Kernmuster gehört zur Lektion "${lessonTitle}".` }; } function buildSpeakingExercise(lessonTitle, didactics, fallbackPattern) { const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null; const expectedText = normalizeText(speakingPrompt?.cue || fallbackPattern); if (!expectedText) return null; const keywords = expectedText .toLowerCase() .replace(/[.,?!]/g, '') .split(' ') .filter((token) => token.length >= 3) .slice(0, 5); return { exerciseTypeId: 8, title: `${lessonTitle}: Frei sprechen`, instruction: 'Sprich das zentrale deutsche Muster frei nach.', questionData: { type: 'speaking_from_memory', question: speakingPrompt?.prompt || `Sprich ein zentrales Muster aus der Lektion "${lessonTitle}".`, expectedText, keywords }, answerData: { type: 'speaking_from_memory' }, explanation: 'Wichtig sind ein flüssiger Abruf und die zentralen deutschen Schlüsselwörter.' }; } function buildSituationalExercise(lessonTitle, didactics, fallbackPattern) { const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null; const modelAnswer = normalizeText(speakingPrompt?.cue || fallbackPattern); if (!modelAnswer) return null; const keywords = modelAnswer .toLowerCase() .replace(/[.,?!]/g, '') .split(' ') .filter((token) => token.length >= 3) .slice(0, 5); return withTypeName('situational_response', { title: `${lessonTitle}: Situativ reagieren`, instruction: 'Antworte kurz und passend auf Deutsch.', questionData: { type: 'situational_response', question: speakingPrompt?.prompt || `Reagiere passend mit einem Ausdruck aus der Lektion "${lessonTitle}".`, keywords }, answerData: { modelAnswer, keywords }, explanation: `Das Kernmuster "${modelAnswer}" passt natürlich zu dieser Situation.` }); } function buildCultureExercise(lesson, pattern, allPatterns) { const distractors = pickDistractors(pattern, allPatterns, 3); if (distractors.length < 3) return null; const options = rotateArray([pattern, ...distractors], simpleHash(`${lesson.title}:culture`) % 4); const culturalNote = normalizeText(lesson.culturalNotes || ''); return { exerciseTypeId: 2, title: `${lesson.title}: Kulturell einordnen`, instruction: 'Ordne den Ausdruck dem kulturellen Schwerpunkt der Lektion zu.', questionData: { type: 'multiple_choice', question: culturalNote ? `${culturalNote} Welche Formulierung passt dazu besonders gut?` : `Welche Formulierung passt besonders gut zum Schwerpunkt "${lesson.title}"?`, options }, answerData: { type: 'multiple_choice', correctAnswer: options.indexOf(pattern) }, explanation: `"${pattern}" ist eng mit dem kulturellen Schwerpunkt dieser Lektion verbunden.` }; } function generateExercisesFromDidactics(lesson) { const didactics = getLessonDidactics(lesson); const corePatterns = didactics.corePatterns; if (corePatterns.length === 0) return []; const patternA = corePatterns[0]; const patternB = corePatterns[1] || corePatterns[0]; const pool = Array.from(new Set([...corePatterns, ...GENERIC_DISTRACTOR_PATTERNS])); if (lesson.lessonType === 'conversation') { return [ buildChoiceExercise(lesson, didactics, patternA, pool, 0), buildGapExercise(lesson.title, patternA), buildSentenceExercise(lesson.title, patternB), buildSituationalExercise(lesson.title, didactics, patternA), buildSpeakingExercise(lesson.title, didactics, patternB) ].filter(Boolean); } if (lesson.lessonType === 'grammar') { return [ buildChoiceExercise(lesson, didactics, patternA, pool, 0), buildChoiceExercise(lesson, didactics, patternB, pool, 1), buildGapExercise(lesson.title, patternA), buildSentenceExercise(lesson.title, patternB), buildSpeakingExercise(lesson.title, didactics, patternA) ].filter(Boolean); } if (lesson.lessonType === 'review' || lesson.didacticMode === 'intensive_review') { return [ buildChoiceExercise(lesson, didactics, patternA, pool, 0), buildChoiceExercise(lesson, didactics, patternB, pool, 1), buildGapExercise(lesson.title, patternA), buildSentenceExercise(lesson.title, patternB), buildSituationalExercise(lesson.title, didactics, patternA), buildSpeakingExercise(lesson.title, didactics, patternB) ].filter(Boolean); } if (lesson.lessonType === 'culture') { return [ buildCultureExercise(lesson, patternA, pool), buildGapExercise(lesson.title, patternA), buildSpeakingExercise(lesson.title, didactics, patternB) ].filter(Boolean); } return [ buildChoiceExercise(lesson, didactics, patternA, pool, 0), buildChoiceExercise(lesson, didactics, patternB, pool, 1), buildGapExercise(lesson.title, patternA), buildSentenceExercise(lesson.title, patternB) ].filter(Boolean); } const GERMAN_EXERCISES = { 'Begrüßung & Vorstellung': [ { exerciseTypeId: 2, title: 'Vorstellung erkennen', instruction: 'Wähle die passendste deutsche Vorstellung.', questionData: { type: 'multiple_choice', question: 'Du triffst jemanden zum ersten Mal. Welche Formulierung passt?', options: ['Hallo, ich heiße Maria.', 'Ich habe Maria.', 'Wo Maria?', 'Nicht Maria.'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Hallo, ich heiße Maria." ist eine natürliche deutsche Selbstvorstellung.' }, { exerciseTypeId: 1, title: 'Herkunft ergänzen', instruction: 'Fülle die Lücke mit dem passenden Ausdruck.', questionData: { type: 'gap_fill', text: 'Ich komme {gap} Cebu.', gaps: 1 }, answerData: { type: 'gap_fill', answers: ['aus'] }, explanation: 'Im Deutschen sagt man "Ich komme aus Cebu."' }, withTypeName('situational_response', { title: 'Kurz reagieren bei der Begrüßung', instruction: 'Antworte kurz und passend auf Deutsch.', questionData: { type: 'situational_response', question: 'Jemand sagt: "Hallo, ich heiße Anna." Wie reagierst du mit deiner eigenen Vorstellung?', keywords: ['hallo', 'heiße', 'ich'] }, answerData: { modelAnswer: 'Hallo, ich heiße Maria.', keywords: ['hallo', 'heiße', 'ich'] }, explanation: 'Die sicherste frühe Reaktion ist eine eigene kurze Vorstellung mit "ich heiße".' }) ], 'der / die / das - Einstieg': [ { exerciseTypeId: 2, title: 'Artikel als Chunk erkennen', instruction: 'Wähle die Form mit dem richtigen Artikel.', questionData: { type: 'multiple_choice', question: 'Welche Form ist als Lern-Chunk richtig?', options: ['der Tisch', 'die Tisch', 'das Tasche', 'der Kind'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: 'Nomen sollen möglichst zusammen mit dem Artikel gelernt werden.' }, { exerciseTypeId: 1, title: 'Artikel ergänzen', instruction: 'Fülle den passenden Artikel ein.', questionData: { type: 'gap_fill', text: '{gap} Tasche', gaps: 1 }, answerData: { type: 'gap_fill', answers: ['die'] }, explanation: 'Es heißt "die Tasche".' }, { exerciseTypeId: 3, title: 'Chunk richtig bauen', instruction: 'Ordne die Wörter zu einem korrekten Lern-Chunk.', questionData: { type: 'sentence_building', question: 'Baue den richtigen Chunk für "child".', tokens: ['das', 'Kind'] }, answerData: { correct: ['das Kind'] }, explanation: 'Auch sehr kurze Strukturen sollen als vollständiger Chunk gelernt werden.' } ], 'nicht / kein - Einstieg': [ { exerciseTypeId: 2, title: 'Negation unterscheiden', instruction: 'Wähle die passende deutsche Negation.', questionData: { type: 'multiple_choice', question: 'Welche Formulierung ist richtig?', options: ['Ich habe kein Geld.', 'Ich habe nicht Geld.', 'Ich bin kein müde.', 'Ich kein habe Geld.'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Kein" steht hier vor dem Nomen, deshalb ist "Ich habe kein Geld." richtig.' }, { exerciseTypeId: 2, title: 'Adjektiv negieren', instruction: 'Wähle die richtige Form für eine Aussage mit Adjektiv.', questionData: { type: 'multiple_choice', question: 'Wie sagst du korrekt: "Ich bin nicht müde"?', options: ['Ich bin nicht müde.', 'Ich bin kein müde.', 'Ich nicht bin müde.', 'Ich bin müde kein.'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: 'Adjektive und Aussagen werden im Deutschen oft mit "nicht" negiert.' }, { exerciseTypeId: 1, title: 'Kein ergänzen', instruction: 'Ergänze die passende Negation.', questionData: { type: 'gap_fill', text: 'Ich habe {gap} Zeit.', gaps: 1 }, answerData: { type: 'gap_fill', answers: ['keine'] }, explanation: 'Bei "Zeit" braucht man hier "keine".' } ], 'wo / wohin - Kontrast': [ { exerciseTypeId: 2, title: 'Ort oder Richtung?', instruction: 'Wähle die Frage nach einem Ziel.', questionData: { type: 'multiple_choice', question: 'Welche Frage passt, wenn du nach dem Ziel fragst?', options: ['Wohin gehst du?', 'Wo gehst du?', 'Wie gehst du?', 'Wann gehst du?'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Wohin" fragt nach einer Richtung oder einem Ziel.' }, { exerciseTypeId: 1, title: 'Fragewort ergänzen', instruction: 'Setze das passende Fragewort ein.', questionData: { type: 'gap_fill', text: '{gap} bist du? Ich bin zu Hause.', gaps: 1 }, answerData: { type: 'gap_fill', answers: ['Wo'] }, explanation: 'Bei einem Ort fragt man im Deutschen mit "Wo".' } ], 'du / Sie - Einstieg': [ { exerciseTypeId: 2, title: 'Höfliche Anrede erkennen', instruction: 'Wähle die höfliche deutsche Form.', questionData: { type: 'multiple_choice', question: 'Du sprichst eine fremde erwachsene Person höflich an. Welche Frage passt?', options: ['Wie heißen Sie?', 'Wie heißt du?', 'Wo bist du?', 'Wie bist du?'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Sie" markiert im Deutschen die höfliche Anrede.' }, { exerciseTypeId: 1, title: 'Anredeform ergänzen', instruction: 'Fülle die passende Anredeform ein.', questionData: { type: 'gap_fill', text: 'Können {gap} mir helfen?', gaps: 1 }, answerData: { type: 'gap_fill', answers: ['Sie'] }, explanation: 'In einer höflichen Bitte heißt es "Können Sie mir helfen?".' } ], 'Perfekt - Einstieg': [ { exerciseTypeId: 2, title: 'Perfektform erkennen', instruction: 'Wähle die passende deutsche Vergangenheitsform.', questionData: { type: 'multiple_choice', question: 'Welche Form ist ein korrektes frühes Perfekt?', options: ['Ich habe gearbeitet.', 'Ich habe arbeiten.', 'Ich bin gearbeitet.', 'Ich arbeitete habe.'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: 'Viele frühe Alltagsverben bilden das Perfekt mit "haben" + Partizip II.' }, { exerciseTypeId: 1, title: 'Hilfsverb ergänzen', instruction: 'Ergänze das passende Hilfsverb.', questionData: { type: 'gap_fill', text: 'Ich {gap} gestern gearbeitet.', gaps: 1 }, answerData: { type: 'gap_fill', answers: ['habe'] }, explanation: 'Im Perfekt braucht man hier das Hilfsverb "habe".' } ] }; async function resolveExerciseTypeId(exercise) { if (exercise.exerciseTypeId) { return exercise.exerciseTypeId; } const trimmedName = exercise.exerciseTypeName != null && exercise.exerciseTypeName !== '' ? String(exercise.exerciseTypeName).trim() : ''; if (!trimmedName) { throw new Error(`Kein exerciseTypeId oder exerciseTypeName für Übung "${exercise.title || 'unbenannt'}" definiert`); } const [type] = await sequelize.query( `SELECT id FROM community.vocab_grammar_exercise_type WHERE name = :name LIMIT 1`, { replacements: { name: trimmedName }, type: sequelize.QueryTypes.SELECT } ); if (!type) { throw new Error(`Übungstyp "${trimmedName}" nicht gefunden`); } return Number(type.id); } async function findOrCreateSystemUser() { let systemUser = await User.findOne({ where: { username: 'system' } }); if (!systemUser) { systemUser = await User.findOne({ where: { username: 'admin' } }); } if (!systemUser) { throw new Error('System user not found'); } return systemUser; } function getExercisesForLesson(lesson) { if (GERMAN_EXERCISES[lesson.title]) { return GERMAN_EXERCISES[lesson.title]; } for (const [key, exercises] of Object.entries(GERMAN_EXERCISES)) { if (lesson.title.includes(key) || key.includes(lesson.title)) { return exercises; } } return generateExercisesFromDidactics(lesson); } async function createGermanForBisayaCourseContent() { await sequelize.authenticate(); console.log('Datenbankverbindung erfolgreich hergestellt.\n'); const systemUser = await findOrCreateSystemUser(); console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`); const [germanLanguage] = await sequelize.query( `SELECT id FROM community.vocab_language WHERE name = 'Deutsch' LIMIT 1`, { type: sequelize.QueryTypes.SELECT } ); const [bisayaLanguage] = await sequelize.query( `SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`, { type: sequelize.QueryTypes.SELECT } ); if (!germanLanguage || !bisayaLanguage) { throw new Error('Deutsch oder Bisaya als Sprache nicht gefunden'); } const courses = await sequelize.query( `SELECT id, title, owner_user_id AS "ownerUserId" FROM community.vocab_course WHERE language_id = :languageId AND native_language_id = :nativeLanguageId AND title LIKE 'Deutsch für Bisaya-Lernende%'`, { replacements: { languageId: germanLanguage.id, nativeLanguageId: bisayaLanguage.id }, type: sequelize.QueryTypes.SELECT } ); console.log(`Gefunden: ${courses.length} Deutsch-für-Bisaya-Kurse\n`); let totalExercisesAdded = 0; let totalLessonsProcessed = 0; const replaceLessons = new Set(Object.keys(GERMAN_EXERCISES)); for (const course of courses) { console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`); const lessons = await VocabCourseLesson.findAll({ where: { courseId: course.id }, order: [['lessonNumber', 'ASC']] }); console.log(` ${lessons.length} Lektionen gefunden\n`); for (const lesson of lessons) { const exercises = getExercisesForLesson(lesson); if (exercises.length === 0) { console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen definiert`); continue; } const existingCount = await VocabGrammarExercise.count({ where: { lessonId: lesson.id } }); const replaceExisting = replaceLessons.has(lesson.title); if (existingCount > 0 && !replaceExisting) { console.log(` ⏭️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - bereits ${existingCount} Übung(en) vorhanden`); continue; } if (replaceExisting && existingCount > 0) { const deleted = await VocabGrammarExercise.destroy({ where: { lessonId: lesson.id } }); console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deleted} bestehende Übung(en) entfernt`); } let exerciseNumber = 1; for (const exerciseData of exercises) { const exerciseTypeId = await resolveExerciseTypeId(exerciseData); await VocabGrammarExercise.create({ lessonId: lesson.id, exerciseTypeId, exerciseNumber: exerciseNumber++, title: exerciseData.title, instruction: exerciseData.instruction, questionData: JSON.stringify(exerciseData.questionData), answerData: JSON.stringify(exerciseData.answerData), explanation: exerciseData.explanation, createdByUserId: course.ownerUserId || systemUser.id }); totalExercisesAdded++; } console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} Übung(en) erstellt`); totalLessonsProcessed++; } console.log(''); } console.log('\n🎉 Zusammenfassung:'); console.log(` ${totalLessonsProcessed} Lektionen bearbeitet`); console.log(` ${totalExercisesAdded} Übungen erstellt`); } createGermanForBisayaCourseContent() .then(() => { sequelize.close(); process.exit(0); }) .catch((error) => { console.error('❌ Fehler:', error); sequelize.close(); process.exit(1); });