Files
breakpilot-lehrer/backend-lehrer/content_generators/pdf_generator.py
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

530 lines
16 KiB
Python

"""
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