fix: Restore all files lost during destructive rebase

A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,34 @@
"""
Content Generators for Breakpilot
=================================
Generates H5P activities and PDF worksheets from unit definitions.
"""
from .h5p_generator import (
H5PGenerator,
H5PContent,
generate_h5p_for_unit,
generate_h5p_manifest,
)
from .pdf_generator import (
PDFGenerator,
Worksheet,
WorksheetSection,
generate_worksheet_html,
generate_worksheet_pdf,
)
__all__ = [
# H5P
"H5PGenerator",
"H5PContent",
"generate_h5p_for_unit",
"generate_h5p_manifest",
# PDF
"PDFGenerator",
"Worksheet",
"WorksheetSection",
"generate_worksheet_html",
"generate_worksheet_pdf",
]

View File

@@ -0,0 +1,429 @@
"""
H5P Content Generator for Breakpilot Units
==========================================
Generates H5P-compatible content structures from unit definitions.
Supported H5P content types:
- H5P.DragQuestion (Drag and Drop)
- H5P.Blanks (Fill in the Blanks)
- H5P.MultiChoice (Multiple Choice)
- H5P.ImageHotspots (Interactive Images)
"""
import json
import uuid
from dataclasses import dataclass, field
from typing import Any, Optional
from pathlib import Path
@dataclass
class H5PContent:
"""Represents an H5P content package"""
content_type: str
title: str
content: dict
metadata: dict = field(default_factory=dict)
def to_json(self) -> str:
return json.dumps({
"contentType": self.content_type,
"title": self.title,
"content": self.content,
"metadata": self.metadata
}, indent=2, ensure_ascii=False)
def to_h5p_structure(self) -> dict:
"""Generate H5P-compatible structure for export"""
return {
"h5p": {
"mainLibrary": self.content_type,
"title": self.title,
"language": "de",
"embedTypes": ["div"],
"license": "U",
"preloadedDependencies": self._get_dependencies()
},
"content": self.content
}
def _get_dependencies(self) -> list:
"""Get required H5P libraries for content type"""
deps = {
"H5P.DragQuestion": [
{"machineName": "H5P.DragQuestion", "majorVersion": 1, "minorVersion": 14}
],
"H5P.Blanks": [
{"machineName": "H5P.Blanks", "majorVersion": 1, "minorVersion": 14}
],
"H5P.MultiChoice": [
{"machineName": "H5P.MultiChoice", "majorVersion": 1, "minorVersion": 16}
],
"H5P.ImageHotspots": [
{"machineName": "H5P.ImageHotspots", "majorVersion": 1, "minorVersion": 10}
]
}
return deps.get(self.content_type, [])
class H5PGenerator:
"""Generates H5P content from unit definitions"""
def __init__(self, locale: str = "de-DE"):
self.locale = locale
def generate_from_unit(self, unit: dict) -> list[H5PContent]:
"""
Generate all H5P content items from a unit definition.
Args:
unit: Unit definition dictionary
Returns:
List of H5PContent objects
"""
contents = []
# Generate drag and drop from stops/vocab
drag_drop = self._generate_drag_drop(unit)
if drag_drop:
contents.append(drag_drop)
# Generate fill in blanks from concepts
blanks = self._generate_fill_blanks(unit)
if blanks:
contents.append(blanks)
# Generate multiple choice from pre/post check questions
mc_items = self._generate_multiple_choice(unit)
contents.extend(mc_items)
return contents
def _get_localized(self, obj: Optional[dict], key: str, default: str = "") -> str:
"""Get localized string from object"""
if not obj:
return default
if isinstance(obj, str):
return obj
if isinstance(obj, dict):
# Try locale-specific key
if self.locale in obj:
return obj[self.locale]
# Try without hyphen (de_DE vs de-DE)
alt_locale = self.locale.replace("-", "_")
if alt_locale in obj:
return obj[alt_locale]
# Fallback to first available
if obj:
return next(iter(obj.values()))
return default
def _generate_drag_drop(self, unit: dict) -> Optional[H5PContent]:
"""
Generate Drag and Drop content from unit vocabulary.
Students drag terms to their definitions.
"""
stops = unit.get("stops", [])
if not stops:
return None
# Collect all vocabulary items
vocab_items = []
for stop in stops:
label = self._get_localized(stop.get("label"), self.locale)
vocab = stop.get("vocab", [])
for v in vocab:
term = self._get_localized(v.get("term"), self.locale)
hint = self._get_localized(v.get("hint"), self.locale)
if term and hint:
vocab_items.append({
"term": term,
"definition": hint,
"stop": label
})
if len(vocab_items) < 2:
return None
# Build H5P.DragQuestion content
drop_zones = []
draggables = []
for i, item in enumerate(vocab_items[:8]): # Limit to 8 items
# Drop zone for definition
drop_zones.append({
"x": 10,
"y": 10 + i * 12,
"width": 35,
"height": 10,
"correctElements": [str(i)],
"showLabel": True,
"backgroundOpacity": 100,
"tipsAndFeedback": {
"tip": f"Ziehe den passenden Begriff hierher"
},
"label": f"<p>{item['definition']}</p>"
})
# Draggable term
draggables.append({
"x": 60,
"y": 10 + i * 12,
"width": 30,
"height": 8,
"dropZones": [str(i)],
"type": {
"library": "H5P.AdvancedText 1.1",
"params": {
"text": f"<p>{item['term']}</p>"
}
},
"multiple": False
})
unit_title = self._get_localized(unit.get("title"), self.locale, unit.get("unit_id", "Unit"))
content = {
"scoreShow": "Ergebnis anzeigen",
"tryAgain": "Nochmal versuchen",
"checkAnswer": "Antworten pruefen",
"submitAnswers": "Abgeben",
"behaviour": {
"enableRetry": True,
"enableCheckButton": True,
"showSolutionsRequiresInput": True,
"singlePoint": False,
"applyPenalties": False,
"enableScoreExplanation": True,
"dropZoneHighlighting": "dragging",
"autoAlignSpacing": 2,
"enableFullScreen": False
},
"question": {
"settings": {
"size": {
"width": 620,
"height": 400
}
},
"task": {
"elements": draggables,
"dropZones": drop_zones
}
},
"overallFeedback": [
{"from": 0, "to": 50, "feedback": "Versuche es nochmal!"},
{"from": 51, "to": 80, "feedback": "Gut gemacht!"},
{"from": 81, "to": 100, "feedback": "Ausgezeichnet!"}
]
}
return H5PContent(
content_type="H5P.DragQuestion",
title=f"{unit_title} - Begriffe zuordnen",
content=content,
metadata={
"unit_id": unit.get("unit_id"),
"generated_from": "vocabulary"
}
)
def _generate_fill_blanks(self, unit: dict) -> Optional[H5PContent]:
"""
Generate Fill in the Blanks from concept "why" explanations.
Key terms become blanks.
"""
stops = unit.get("stops", [])
if not stops:
return None
sentences = []
for stop in stops:
concept = stop.get("concept", {})
why = self._get_localized(concept.get("why"), self.locale)
vocab = stop.get("vocab", [])
if not why or not vocab:
continue
# Find terms to blank out
sentence = why
for v in vocab:
term = self._get_localized(v.get("term"), self.locale)
if term and term.lower() in sentence.lower():
# Replace term with blank (H5P format: *term*)
import re
pattern = re.compile(re.escape(term), re.IGNORECASE)
sentence = pattern.sub(f"*{term}*", sentence, count=1)
break
if "*" in sentence:
sentences.append(sentence)
if not sentences:
return None
unit_title = self._get_localized(unit.get("title"), self.locale, unit.get("unit_id", "Unit"))
content = {
"text": "<p>" + "</p><p>".join(sentences) + "</p>",
"overallFeedback": [
{"from": 0, "to": 50, "feedback": "Versuche es nochmal!"},
{"from": 51, "to": 80, "feedback": "Gut gemacht!"},
{"from": 81, "to": 100, "feedback": "Ausgezeichnet!"}
],
"showSolutions": "Loesung anzeigen",
"tryAgain": "Nochmal versuchen",
"checkAnswer": "Pruefen",
"submitAnswer": "Abgeben",
"notFilledOut": "Bitte fuellen Sie alle Luecken aus.",
"answerIsCorrect": "':ans' ist richtig!",
"answerIsWrong": "':ans' ist falsch.",
"answeredCorrectly": "Richtig!",
"answeredIncorrectly": "Falsch.",
"behaviour": {
"enableRetry": True,
"enableSolutionsButton": True,
"enableCheckButton": True,
"caseSensitive": False,
"autoCheck": False,
"separateLines": False
}
}
return H5PContent(
content_type="H5P.Blanks",
title=f"{unit_title} - Lueckentext",
content=content,
metadata={
"unit_id": unit.get("unit_id"),
"generated_from": "concepts"
}
)
def _generate_multiple_choice(self, unit: dict) -> list[H5PContent]:
"""
Generate Multiple Choice questions from precheck/postcheck.
"""
contents = []
# Load precheck questions if referenced
precheck = unit.get("precheck", {})
postcheck = unit.get("postcheck", {})
# For now, generate from stops' concepts as misconception-targeted questions
stops = unit.get("stops", [])
unit_title = self._get_localized(unit.get("title"), self.locale, unit.get("unit_id", "Unit"))
for i, stop in enumerate(stops):
concept = stop.get("concept", {})
misconception = self._get_localized(concept.get("common_misconception"), self.locale)
why = self._get_localized(concept.get("why"), self.locale)
label = self._get_localized(stop.get("label"), self.locale)
if not misconception or not why:
continue
# Create a question about the concept
content = {
"question": f"<p>Was ist richtig ueber {label}?</p>",
"answers": [
{
"correct": True,
"text": f"<p>{why}</p>",
"tpiAndFeedback": {
"chosenFeedback": "<p>Richtig!</p>",
"notChosenFeedback": ""
}
},
{
"correct": False,
"text": f"<p>{misconception}</p>",
"tipsAndFeedback": {
"chosenFeedback": f"<p>Das ist ein haeufiger Irrtum!</p>",
"notChosenFeedback": ""
}
}
],
"overallFeedback": [
{"from": 0, "to": 50, "feedback": "Versuche es nochmal!"},
{"from": 51, "to": 100, "feedback": "Sehr gut!"}
],
"behaviour": {
"enableRetry": True,
"enableSolutionsButton": True,
"enableCheckButton": True,
"type": "auto",
"singlePoint": True,
"randomAnswers": True,
"showSolutionsRequiresInput": True,
"confirmCheckDialog": False,
"confirmRetryDialog": False,
"autoCheck": False,
"passPercentage": 100
},
"UI": {
"checkAnswerButton": "Pruefen",
"submitAnswerButton": "Abgeben",
"showSolutionButton": "Loesung zeigen",
"tryAgainButton": "Nochmal",
"tipsLabel": "Tipp anzeigen",
"scoreBarLabel": "Du hast :num von :total Punkten",
"tipAvailable": "Tipp verfuegbar",
"feedbackAvailable": "Feedback verfuegbar",
"readFeedback": "Feedback lesen",
"wrongAnswer": "Falsche Antwort",
"correctAnswer": "Richtige Antwort"
}
}
contents.append(H5PContent(
content_type="H5P.MultiChoice",
title=f"{unit_title} - {label} Quiz",
content=content,
metadata={
"unit_id": unit.get("unit_id"),
"stop_id": stop.get("stop_id"),
"generated_from": "misconception"
}
))
return contents
def generate_h5p_for_unit(unit_definition: dict, locale: str = "de-DE") -> list[dict]:
"""
Main function to generate H5P content for a unit.
Args:
unit_definition: The unit JSON definition
locale: Target locale for content
Returns:
List of H5P content structures ready for export
"""
generator = H5PGenerator(locale=locale)
contents = generator.generate_from_unit(unit_definition)
return [c.to_h5p_structure() for c in contents]
def generate_h5p_manifest(contents: list[H5PContent], unit_id: str) -> dict:
"""
Generate a manifest listing all H5P content for a unit.
"""
return {
"unit_id": unit_id,
"generated_at": __import__("datetime").datetime.utcnow().isoformat(),
"content_count": len(contents),
"items": [
{
"id": str(uuid.uuid4()),
"type": c.content_type,
"title": c.title,
"metadata": c.metadata
}
for c in contents
]
}

View File

@@ -0,0 +1,529 @@
"""
PDF Worksheet Generator for Breakpilot Units
============================================
Generates printable PDF worksheets from unit definitions.
Structure:
1. Title + Learning Objectives
2. Vocabulary Table
3. Key Concepts Summary
4. Practice Exercises (Basic)
5. Challenge Exercises (Advanced)
6. Reflection Questions
"""
import io
from dataclasses import dataclass
from typing import Any, Optional, Union
# Note: In production, use reportlab or weasyprint for actual PDF generation
# This module generates an intermediate format that can be converted to PDF
@dataclass
class WorksheetSection:
"""A section of the worksheet"""
title: str
content_type: str # "text", "table", "exercises", "blanks"
content: Any
difficulty: int = 1 # 1-4
@dataclass
class Worksheet:
"""Complete worksheet structure"""
title: str
subtitle: str
unit_id: str
locale: str
sections: list[WorksheetSection]
footer: str = ""
def to_html(self) -> str:
"""Convert worksheet to HTML (for PDF conversion via weasyprint)"""
html_parts = [
"<!DOCTYPE html>",
"<html lang='de'>",
"<head>",
"<meta charset='UTF-8'>",
"<style>",
self._get_styles(),
"</style>",
"</head>",
"<body>",
f"<header><h1>{self.title}</h1>",
f"<p class='subtitle'>{self.subtitle}</p></header>",
]
for section in self.sections:
html_parts.append(self._render_section(section))
html_parts.extend([
f"<footer>{self.footer}</footer>",
"</body>",
"</html>"
])
return "\n".join(html_parts)
def _get_styles(self) -> str:
return """
@page {
size: A4;
margin: 2cm;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 11pt;
line-height: 1.5;
color: #333;
}
header {
text-align: center;
margin-bottom: 1.5em;
border-bottom: 2px solid #2c5282;
padding-bottom: 1em;
}
h1 {
color: #2c5282;
margin-bottom: 0.25em;
font-size: 20pt;
}
.subtitle {
color: #666;
font-style: italic;
}
h2 {
color: #2c5282;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 0.25em;
margin-top: 1.5em;
font-size: 14pt;
}
h3 {
color: #4a5568;
font-size: 12pt;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
}
th, td {
border: 1px solid #e2e8f0;
padding: 0.5em;
text-align: left;
}
th {
background-color: #edf2f7;
font-weight: bold;
}
.exercise {
margin: 1em 0;
padding: 1em;
background-color: #f7fafc;
border-left: 4px solid #4299e1;
}
.exercise-number {
font-weight: bold;
color: #2c5282;
}
.blank {
display: inline-block;
min-width: 100px;
border-bottom: 1px solid #333;
margin: 0 0.25em;
}
.difficulty {
font-size: 9pt;
color: #718096;
}
.difficulty-1 { color: #48bb78; }
.difficulty-2 { color: #4299e1; }
.difficulty-3 { color: #ed8936; }
.difficulty-4 { color: #f56565; }
.reflection {
margin-top: 2em;
padding: 1em;
background-color: #fffaf0;
border: 1px dashed #ed8936;
}
.write-area {
min-height: 80px;
border: 1px solid #e2e8f0;
margin: 0.5em 0;
background-color: #fff;
}
footer {
margin-top: 2em;
padding-top: 1em;
border-top: 1px solid #e2e8f0;
font-size: 9pt;
color: #718096;
text-align: center;
}
ul, ol {
margin: 0.5em 0;
padding-left: 1.5em;
}
.objectives {
background-color: #ebf8ff;
padding: 1em;
border-radius: 4px;
}
"""
def _render_section(self, section: WorksheetSection) -> str:
parts = [f"<section><h2>{section.title}</h2>"]
if section.content_type == "text":
parts.append(f"<p>{section.content}</p>")
elif section.content_type == "objectives":
parts.append("<div class='objectives'><ul>")
for obj in section.content:
parts.append(f"<li>{obj}</li>")
parts.append("</ul></div>")
elif section.content_type == "table":
parts.append("<table><thead><tr>")
for header in section.content.get("headers", []):
parts.append(f"<th>{header}</th>")
parts.append("</tr></thead><tbody>")
for row in section.content.get("rows", []):
parts.append("<tr>")
for cell in row:
parts.append(f"<td>{cell}</td>")
parts.append("</tr>")
parts.append("</tbody></table>")
elif section.content_type == "exercises":
for i, ex in enumerate(section.content, 1):
diff_class = f"difficulty-{ex.get('difficulty', 1)}"
diff_stars = "*" * ex.get("difficulty", 1)
parts.append(f"""
<div class='exercise'>
<span class='exercise-number'>Aufgabe {i}</span>
<span class='difficulty {diff_class}'>({diff_stars})</span>
<p>{ex.get('question', '')}</p>
{self._render_exercise_input(ex)}
</div>
""")
elif section.content_type == "blanks":
text = section.content
# Replace *word* with blank
import re
text = re.sub(r'\*([^*]+)\*', r"<span class='blank'></span>", text)
parts.append(f"<p>{text}</p>")
elif section.content_type == "reflection":
parts.append("<div class='reflection'>")
parts.append(f"<p><strong>{section.content.get('prompt', '')}</strong></p>")
parts.append("<div class='write-area'></div>")
parts.append("</div>")
parts.append("</section>")
return "\n".join(parts)
def _render_exercise_input(self, exercise: dict) -> str:
ex_type = exercise.get("type", "text")
if ex_type == "multiple_choice":
options = exercise.get("options", [])
parts = ["<ul style='list-style-type: none;'>"]
for opt in options:
parts.append(f"<li>&#9633; {opt}</li>")
parts.append("</ul>")
return "\n".join(parts)
elif ex_type == "matching":
left = exercise.get("left", [])
right = exercise.get("right", [])
parts = ["<table><tr><th>Begriff</th><th>Zuordnung</th></tr>"]
for i, item in enumerate(left):
right_item = right[i] if i < len(right) else ""
parts.append(f"<tr><td>{item}</td><td class='blank' style='width:200px'></td></tr>")
parts.append("</table>")
return "\n".join(parts)
elif ex_type == "sequence":
items = exercise.get("items", [])
parts = ["<p>Bringe in die richtige Reihenfolge:</p><ol>"]
for item in items:
parts.append(f"<li class='blank' style='min-width:200px'></li>")
parts.append("</ol>")
parts.append(f"<p style='font-size:9pt;color:#718096'>Begriffe: {', '.join(items)}</p>")
return "\n".join(parts)
else:
return "<div class='write-area'></div>"
class PDFGenerator:
"""Generates PDF worksheets from unit definitions"""
def __init__(self, locale: str = "de-DE"):
self.locale = locale
def generate_from_unit(self, unit: dict) -> Worksheet:
"""
Generate a worksheet from a unit definition.
Args:
unit: Unit definition dictionary
Returns:
Worksheet object
"""
unit_id = unit.get("unit_id", "unknown")
title = self._get_localized(unit.get("title"), "Arbeitsblatt")
objectives = unit.get("learning_objectives", [])
stops = unit.get("stops", [])
sections = []
# Learning Objectives
if objectives:
sections.append(WorksheetSection(
title="Lernziele",
content_type="objectives",
content=objectives
))
# Vocabulary Table
vocab_section = self._create_vocabulary_section(stops)
if vocab_section:
sections.append(vocab_section)
# Key Concepts Summary
concepts_section = self._create_concepts_section(stops)
if concepts_section:
sections.append(concepts_section)
# Basic Exercises
basic_exercises = self._create_basic_exercises(stops)
if basic_exercises:
sections.append(WorksheetSection(
title="Ubungen - Basis",
content_type="exercises",
content=basic_exercises,
difficulty=1
))
# Challenge Exercises
challenge_exercises = self._create_challenge_exercises(stops, unit)
if challenge_exercises:
sections.append(WorksheetSection(
title="Ubungen - Herausforderung",
content_type="exercises",
content=challenge_exercises,
difficulty=3
))
# Reflection
sections.append(WorksheetSection(
title="Reflexion",
content_type="reflection",
content={
"prompt": "Erklaere in eigenen Worten, was du heute gelernt hast:"
}
))
return Worksheet(
title=title,
subtitle=f"Arbeitsblatt zur Unit: {unit_id}",
unit_id=unit_id,
locale=self.locale,
sections=sections,
footer="Generiert mit Breakpilot | www.breakpilot.de"
)
def _get_localized(self, obj: Union[dict, str, None], default: str = "") -> str:
"""Get localized string from object"""
if not obj:
return default
if isinstance(obj, str):
return obj
if isinstance(obj, dict):
if self.locale in obj:
return obj[self.locale]
alt_locale = self.locale.replace("-", "_")
if alt_locale in obj:
return obj[alt_locale]
if obj:
return next(iter(obj.values()))
return default
def _create_vocabulary_section(self, stops: list) -> Optional[WorksheetSection]:
"""Create vocabulary table from stops"""
rows = []
for stop in stops:
vocab = stop.get("vocab", [])
for v in vocab:
term = self._get_localized(v.get("term"))
hint = self._get_localized(v.get("hint"))
if term:
rows.append([term, hint or ""])
if not rows:
return None
return WorksheetSection(
title="Wichtige Begriffe",
content_type="table",
content={
"headers": ["Begriff", "Erklarung"],
"rows": rows
}
)
def _create_concepts_section(self, stops: list) -> Optional[WorksheetSection]:
"""Create concepts summary from stops"""
rows = []
for stop in stops:
label = self._get_localized(stop.get("label"))
concept = stop.get("concept", {})
why = self._get_localized(concept.get("why"))
if label and why:
rows.append([label, why])
if not rows:
return None
return WorksheetSection(
title="Zusammenfassung",
content_type="table",
content={
"headers": ["Station", "Was hast du gelernt?"],
"rows": rows
}
)
def _create_basic_exercises(self, stops: list) -> list[dict]:
"""Create basic difficulty exercises"""
exercises = []
# Vocabulary matching
vocab_items = []
for stop in stops:
for v in stop.get("vocab", []):
term = self._get_localized(v.get("term"))
hint = self._get_localized(v.get("hint"))
if term and hint:
vocab_items.append({"term": term, "hint": hint})
if len(vocab_items) >= 3:
exercises.append({
"type": "matching",
"question": "Ordne die Begriffe den richtigen Erklarungen zu:",
"left": [v["term"] for v in vocab_items[:5]],
"right": [v["hint"] for v in vocab_items[:5]],
"difficulty": 1
})
# True/False from concepts
for stop in stops[:3]:
concept = stop.get("concept", {})
why = self._get_localized(concept.get("why"))
if why:
exercises.append({
"type": "multiple_choice",
"question": f"Richtig oder Falsch? {why}",
"options": ["Richtig", "Falsch"],
"difficulty": 1
})
break
# Sequence ordering (for FlightPath)
if len(stops) >= 4:
labels = [self._get_localized(s.get("label")) for s in stops[:6] if self._get_localized(s.get("label"))]
if len(labels) >= 4:
import random
shuffled = labels.copy()
random.shuffle(shuffled)
exercises.append({
"type": "sequence",
"question": "Bringe die Stationen in die richtige Reihenfolge:",
"items": shuffled,
"difficulty": 2
})
return exercises
def _create_challenge_exercises(self, stops: list, unit: dict) -> list[dict]:
"""Create challenging exercises"""
exercises = []
# Misconception identification
for stop in stops:
concept = stop.get("concept", {})
misconception = self._get_localized(concept.get("common_misconception"))
why = self._get_localized(concept.get("why"))
label = self._get_localized(stop.get("label"))
if misconception and why:
exercises.append({
"type": "multiple_choice",
"question": f"Welche Aussage uber {label} ist RICHTIG?",
"options": [why, misconception],
"difficulty": 3
})
if len(exercises) >= 2:
break
# Transfer/Application question
exercises.append({
"type": "text",
"question": "Erklaere einem Freund in 2-3 Satzen, was du gelernt hast:",
"difficulty": 3
})
# Critical thinking
exercises.append({
"type": "text",
"question": "Was moechtest du noch mehr uber dieses Thema erfahren?",
"difficulty": 4
})
return exercises
def generate_worksheet_html(unit_definition: dict, locale: str = "de-DE") -> str:
"""
Generate HTML worksheet from unit definition.
Args:
unit_definition: The unit JSON definition
locale: Target locale for content
Returns:
HTML string ready for PDF conversion
"""
generator = PDFGenerator(locale=locale)
worksheet = generator.generate_from_unit(unit_definition)
return worksheet.to_html()
def generate_worksheet_pdf(unit_definition: dict, locale: str = "de-DE") -> bytes:
"""
Generate PDF worksheet from unit definition.
Requires weasyprint to be installed:
pip install weasyprint
Args:
unit_definition: The unit JSON definition
locale: Target locale for content
Returns:
PDF bytes
"""
try:
from weasyprint import HTML
except ImportError:
raise ImportError("weasyprint is required for PDF generation. Install with: pip install weasyprint")
html = generate_worksheet_html(unit_definition, locale)
pdf_bytes = HTML(string=html).write_pdf()
return pdf_bytes