This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/content_generators/pdf_generator.py
Benjamin Admin bfdaf63ba9 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>
2026-02-09 09:51:32 +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