diff --git a/.claude/rules/loc-exceptions.txt b/.claude/rules/loc-exceptions.txt
index c3cced7..d557b94 100644
--- a/.claude/rules/loc-exceptions.txt
+++ b/.claude/rules/loc-exceptions.txt
@@ -39,6 +39,9 @@
**/lib/sdk/vvt-baseline-catalog.ts | owner=admin-lehrer | reason=Pure data catalog (630 LOC, BaselineTemplate[] literals) | review=2027-01-01
**/lib/sdk/loeschfristen-baseline-catalog.ts | owner=admin-lehrer | reason=Pure data catalog (578 LOC, retention period templates) | review=2027-01-01
+# Single SSE generator orchestrating 6 pipeline steps — cannot split generator context
+**/ocr_pipeline_auto_steps.py | owner=klausur | reason=run_auto is a single async generator yielding SSE events across 6 steps (528 LOC) | review=2026-10-01
+
# Legacy — TEMPORAER bis Refactoring abgeschlossen
# Dateien hier werden Phase fuer Phase abgearbeitet und entfernt.
# KEINE neuen Ausnahmen ohne [guardrail-change] Commit-Marker!
diff --git a/backend-lehrer/ai_processing/print_cloze.py b/backend-lehrer/ai_processing/print_cloze.py
new file mode 100644
index 0000000..5b12447
--- /dev/null
+++ b/backend-lehrer/ai_processing/print_cloze.py
@@ -0,0 +1,193 @@
+"""
+AI Processing - Print Version Generator: Cloze (Lueckentext).
+
+Generates printable HTML for cloze/fill-in-the-blank worksheets.
+"""
+
+from pathlib import Path
+import json
+import random
+import logging
+
+from .core import BEREINIGT_DIR
+
+logger = logging.getLogger(__name__)
+
+
+def generate_print_version_cloze(cloze_path: Path, include_answers: bool = False) -> Path:
+ """
+ Generiert eine druckbare HTML-Version der Lueckentexte.
+
+ Args:
+ cloze_path: Pfad zur *_cloze.json Datei
+ include_answers: True fuer Loesungsblatt (fuer Eltern)
+
+ Returns:
+ Pfad zur generierten HTML-Datei
+ """
+ if not cloze_path.exists():
+ raise FileNotFoundError(f"Cloze-Datei nicht gefunden: {cloze_path}")
+
+ cloze_data = json.loads(cloze_path.read_text(encoding="utf-8"))
+ items = cloze_data.get("cloze_items", [])
+ metadata = cloze_data.get("metadata", {})
+
+ title = metadata.get("source_title", "Arbeitsblatt")
+ subject = metadata.get("subject", "")
+ grade = metadata.get("grade_level", "")
+ total_gaps = metadata.get("total_gaps", 0)
+
+ html_parts = []
+ html_parts.append("""
+
+
+
+""" + title + """ - Lueckentext
+
+
+
+""")
+
+ # Header
+ version_text = "Loesungsblatt" if include_answers else "Lueckentext"
+ html_parts.append(f"{title} - {version_text} ")
+ meta_parts = []
+ if subject:
+ meta_parts.append(f"Fach: {subject}")
+ if grade:
+ meta_parts.append(f"Klasse: {grade}")
+ meta_parts.append(f"Luecken gesamt: {total_gaps}")
+ html_parts.append(f"{' | '.join(meta_parts)}
")
+
+ # Sammle alle Lueckenwoerter fuer Wortbank
+ all_words = []
+
+ # Lueckentexte
+ for idx, item in enumerate(items, 1):
+ html_parts.append("")
+ html_parts.append(f"
{idx}.
")
+
+ gaps = item.get("gaps", [])
+ sentence = item.get("sentence_with_gaps", "")
+
+ if include_answers:
+ # Loesungsblatt: Luecken mit Antworten fuellen
+ for gap in gaps:
+ word = gap.get("word", "")
+ sentence = sentence.replace("___", f"
{word} ", 1)
+ else:
+ # Fragenblatt: Luecken als Linien
+ sentence = sentence.replace("___", "
")
+ # Woerter fuer Wortbank sammeln
+ for gap in gaps:
+ all_words.append(gap.get("word", ""))
+
+ html_parts.append(f"
{sentence}
")
+
+ # Uebersetzung anzeigen
+ translation = item.get("translation", {})
+ if translation:
+ lang_name = translation.get("language_name", "Uebersetzung")
+ full_sentence = translation.get("full_sentence", "")
+ if full_sentence:
+ html_parts.append("
")
+ html_parts.append(f"
{lang_name}:
")
+ html_parts.append(full_sentence)
+ html_parts.append("
")
+
+ html_parts.append("
")
+
+ # Wortbank (nur fuer Fragenblatt)
+ if not include_answers and all_words:
+ random.shuffle(all_words) # Mische die Woerter
+ html_parts.append("")
+ html_parts.append("
Wortbank (diese Woerter fehlen):
")
+ for word in all_words:
+ html_parts.append(f"
{word} ")
+ html_parts.append("
")
+
+ html_parts.append("")
+
+ # Speichern
+ suffix = "_cloze_solutions.html" if include_answers else "_cloze_print.html"
+ out_name = cloze_path.stem.replace("_cloze", "") + suffix
+ out_path = BEREINIGT_DIR / out_name
+ out_path.write_text("\n".join(html_parts), encoding="utf-8")
+
+ logger.info(f"Cloze Print-Version gespeichert: {out_path.name}")
+ return out_path
diff --git a/backend-lehrer/ai_processing/print_generator.py b/backend-lehrer/ai_processing/print_generator.py
index df6fc9f..5dd3cb1 100644
--- a/backend-lehrer/ai_processing/print_generator.py
+++ b/backend-lehrer/ai_processing/print_generator.py
@@ -1,824 +1,22 @@
"""
-AI Processing - Print Version Generator.
+AI Processing - Print Version Generator — Barrel Re-export.
-Generiert druckbare HTML-Versionen für verschiedene Arbeitsblatt-Typen.
+Generiert druckbare HTML-Versionen fuer verschiedene Arbeitsblatt-Typen.
+Split into:
+ - print_qa.py: Q&A print generation
+ - print_cloze.py: Cloze/Lueckentext print generation
+ - print_mc.py: Multiple Choice print generation
+ - print_worksheet.py: General worksheet print generation
"""
-from pathlib import Path
-import json
-import random
-import logging
-
-from .core import BEREINIGT_DIR
-
-logger = logging.getLogger(__name__)
-
-
-def generate_print_version_qa(qa_path: Path, include_answers: bool = False) -> Path:
- """
- Generiert eine druckbare HTML-Version der Frage-Antwort-Paare.
-
- Args:
- qa_path: Pfad zur *_qa.json Datei
- include_answers: True für Lösungsblatt (für Eltern)
-
- Returns:
- Pfad zur generierten HTML-Datei
- """
- if not qa_path.exists():
- raise FileNotFoundError(f"Q&A-Datei nicht gefunden: {qa_path}")
-
- qa_data = json.loads(qa_path.read_text(encoding="utf-8"))
- items = qa_data.get("qa_items", [])
- metadata = qa_data.get("metadata", {})
-
- title = metadata.get("source_title", "Arbeitsblatt")
- subject = metadata.get("subject", "")
- grade = metadata.get("grade_level", "")
-
- html_parts = []
- html_parts.append("""
-
-
-
-""" + title + """ - Fragen
-
-
-
-""")
-
- # Header
- version_text = "Lösungsblatt" if include_answers else "Fragenblatt"
- html_parts.append(f"{title} - {version_text} ")
- meta_parts = []
- if subject:
- meta_parts.append(f"Fach: {subject}")
- if grade:
- meta_parts.append(f"Klasse: {grade}")
- meta_parts.append(f"Anzahl Fragen: {len(items)}")
- html_parts.append(f"{' | '.join(meta_parts)}
")
-
- # Fragen
- for idx, item in enumerate(items, 1):
- html_parts.append("")
- html_parts.append(f"
Frage {idx}
")
- html_parts.append(f"
{item.get('question', '')}
")
-
- if include_answers:
- # Lösungsblatt: Antwort anzeigen
- html_parts.append(f"
Antwort: {item.get('answer', '')}
")
- # Schlüsselbegriffe
- key_terms = item.get("key_terms", [])
- if key_terms:
- terms_html = " ".join([f"
{term} " for term in key_terms])
- html_parts.append(f"
Wichtige Begriffe: {terms_html}
")
- else:
- # Fragenblatt: Antwortlinien
- html_parts.append("
")
- for _ in range(3):
- html_parts.append("
")
- html_parts.append("
")
-
- html_parts.append("
")
-
- html_parts.append("")
-
- # Speichern
- suffix = "_qa_solutions.html" if include_answers else "_qa_print.html"
- out_name = qa_path.stem.replace("_qa", "") + suffix
- out_path = BEREINIGT_DIR / out_name
- out_path.write_text("\n".join(html_parts), encoding="utf-8")
-
- logger.info(f"Print-Version gespeichert: {out_path.name}")
- return out_path
-
-
-def generate_print_version_cloze(cloze_path: Path, include_answers: bool = False) -> Path:
- """
- Generiert eine druckbare HTML-Version der Lückentexte.
-
- Args:
- cloze_path: Pfad zur *_cloze.json Datei
- include_answers: True für Lösungsblatt (für Eltern)
-
- Returns:
- Pfad zur generierten HTML-Datei
- """
- if not cloze_path.exists():
- raise FileNotFoundError(f"Cloze-Datei nicht gefunden: {cloze_path}")
-
- cloze_data = json.loads(cloze_path.read_text(encoding="utf-8"))
- items = cloze_data.get("cloze_items", [])
- metadata = cloze_data.get("metadata", {})
-
- title = metadata.get("source_title", "Arbeitsblatt")
- subject = metadata.get("subject", "")
- grade = metadata.get("grade_level", "")
- total_gaps = metadata.get("total_gaps", 0)
-
- html_parts = []
- html_parts.append("""
-
-
-
-""" + title + """ - Lückentext
-
-
-
-""")
-
- # Header
- version_text = "Lösungsblatt" if include_answers else "Lückentext"
- html_parts.append(f"{title} - {version_text} ")
- meta_parts = []
- if subject:
- meta_parts.append(f"Fach: {subject}")
- if grade:
- meta_parts.append(f"Klasse: {grade}")
- meta_parts.append(f"Lücken gesamt: {total_gaps}")
- html_parts.append(f"{' | '.join(meta_parts)}
")
-
- # Sammle alle Lückenwörter für Wortbank
- all_words = []
-
- # Lückentexte
- for idx, item in enumerate(items, 1):
- html_parts.append("")
- html_parts.append(f"
{idx}.
")
-
- gaps = item.get("gaps", [])
- sentence = item.get("sentence_with_gaps", "")
-
- if include_answers:
- # Lösungsblatt: Lücken mit Antworten füllen
- for gap in gaps:
- word = gap.get("word", "")
- sentence = sentence.replace("___", f"
{word} ", 1)
- else:
- # Fragenblatt: Lücken als Linien
- sentence = sentence.replace("___", "
")
- # Wörter für Wortbank sammeln
- for gap in gaps:
- all_words.append(gap.get("word", ""))
-
- html_parts.append(f"
{sentence}
")
-
- # Übersetzung anzeigen
- translation = item.get("translation", {})
- if translation:
- lang_name = translation.get("language_name", "Übersetzung")
- full_sentence = translation.get("full_sentence", "")
- if full_sentence:
- html_parts.append("
")
- html_parts.append(f"
{lang_name}:
")
- html_parts.append(full_sentence)
- html_parts.append("
")
-
- html_parts.append("
")
-
- # Wortbank (nur für Fragenblatt)
- if not include_answers and all_words:
- random.shuffle(all_words) # Mische die Wörter
- html_parts.append("")
- html_parts.append("
Wortbank (diese Wörter fehlen):
")
- for word in all_words:
- html_parts.append(f"
{word} ")
- html_parts.append("
")
-
- html_parts.append("")
-
- # Speichern
- suffix = "_cloze_solutions.html" if include_answers else "_cloze_print.html"
- out_name = cloze_path.stem.replace("_cloze", "") + suffix
- out_path = BEREINIGT_DIR / out_name
- out_path.write_text("\n".join(html_parts), encoding="utf-8")
-
- logger.info(f"Cloze Print-Version gespeichert: {out_path.name}")
- return out_path
-
-
-def generate_print_version_mc(mc_path: Path, include_answers: bool = False) -> str:
- """
- Generiert eine druckbare HTML-Version der Multiple-Choice-Fragen.
-
- Args:
- mc_path: Pfad zur *_mc.json Datei
- include_answers: True für Lösungsblatt mit markierten richtigen Antworten
-
- Returns:
- HTML-String (zum direkten Ausliefern)
- """
- if not mc_path.exists():
- raise FileNotFoundError(f"MC-Datei nicht gefunden: {mc_path}")
-
- mc_data = json.loads(mc_path.read_text(encoding="utf-8"))
- questions = mc_data.get("questions", [])
- metadata = mc_data.get("metadata", {})
-
- title = metadata.get("source_title", "Arbeitsblatt")
- subject = metadata.get("subject", "")
- grade = metadata.get("grade_level", "")
-
- html_parts = []
- html_parts.append("""
-
-
-
-""" + title + """ - Multiple Choice
-
-
-
-""")
-
- # Header
- version_text = "Lösungsblatt" if include_answers else "Multiple Choice Test"
- html_parts.append(f"{title} ")
- html_parts.append(f"{version_text} ")
- if subject:
- html_parts.append(f" | Fach: {subject}")
- if grade:
- html_parts.append(f" | Klasse: {grade}")
- html_parts.append(f" | Anzahl Fragen: {len(questions)}
")
-
- if not include_answers:
- html_parts.append("")
- html_parts.append("Anleitung: Kreuze bei jeder Frage die richtige Antwort an. ")
- html_parts.append("Es ist immer nur eine Antwort richtig.")
- html_parts.append("
")
-
- # Fragen
- for idx, q in enumerate(questions, 1):
- html_parts.append("")
- html_parts.append(f"
Frage {idx}
")
- html_parts.append(f"
{q.get('question', '')}
")
-
- html_parts.append("
")
- correct_answer = q.get("correct_answer", "")
-
- for opt in q.get("options", []):
- opt_id = opt.get("id", "")
- is_correct = opt_id == correct_answer
-
- opt_class = "option"
- checkbox_class = "option-checkbox"
- if include_answers and is_correct:
- opt_class += " option-correct"
- checkbox_class += " checked"
-
- html_parts.append(f"
")
- html_parts.append(f"
")
- html_parts.append(f"
{opt_id}) ")
- html_parts.append(f"
{opt.get('text', '')} ")
- html_parts.append("
")
-
- html_parts.append("
")
-
- # Erklärung nur bei Lösungsblatt
- if include_answers and q.get("explanation"):
- html_parts.append(f"
Erklärung: {q.get('explanation')}
")
-
- html_parts.append("
")
-
- # Lösungsschlüssel (kompakt) - nur bei Lösungsblatt
- if include_answers:
- html_parts.append("")
- html_parts.append("
Lösungsschlüssel
")
- html_parts.append("
")
- for idx, q in enumerate(questions, 1):
- html_parts.append("
")
- html_parts.append(f"{idx}. ")
- html_parts.append(f"{q.get('correct_answer', '')} ")
- html_parts.append("
")
- html_parts.append("
")
- html_parts.append("
")
-
- html_parts.append("")
-
- return "\n".join(html_parts)
-
-
-def generate_print_version_worksheet(analysis_path: Path) -> str:
- """
- Generiert eine druckoptimierte HTML-Version des Arbeitsblatts.
-
- Eigenschaften:
- - Große, gut lesbare Schrift (16pt)
- - Schwarz-weiß / Graustufen-tauglich
- - Klare Struktur für Druck
- - Keine interaktiven Elemente
-
- Args:
- analysis_path: Pfad zur *_analyse.json Datei
-
- Returns:
- HTML-String zum direkten Ausliefern
- """
- if not analysis_path.exists():
- raise FileNotFoundError(f"Analysedatei nicht gefunden: {analysis_path}")
-
- try:
- data = json.loads(analysis_path.read_text(encoding="utf-8"))
- except json.JSONDecodeError as e:
- raise RuntimeError(f"Analyse-Datei enthält kein gültiges JSON: {analysis_path}\n{e}") from e
-
- title = data.get("title") or "Arbeitsblatt"
- subject = data.get("subject") or ""
- grade_level = data.get("grade_level") or ""
- instructions = data.get("instructions") or ""
- tasks = data.get("tasks", []) or []
- canonical_text = data.get("canonical_text") or ""
- printed_blocks = data.get("printed_blocks") or []
-
- html_parts = []
- html_parts.append("""
-
-
-
-""" + title + """
-
-
-
-🖨️ Drucken
-""")
-
- # Titel
- html_parts.append(f"{title} ")
-
- # Meta-Informationen
- meta_parts = []
- if subject:
- meta_parts.append(f"Fach: {subject} ")
- if grade_level:
- meta_parts.append(f"Klasse: {grade_level} ")
- if meta_parts:
- html_parts.append(f"{''.join(meta_parts)}
")
-
- # Arbeitsanweisung
- if instructions:
- html_parts.append("")
- html_parts.append("
Arbeitsanweisung:
")
- html_parts.append(f"
{instructions}
")
- html_parts.append("
")
-
- # Haupttext / gedruckte Blöcke
- if printed_blocks:
- html_parts.append("")
- for block in printed_blocks:
- role = (block.get("role") or "body").lower()
- text = (block.get("text") or "").strip()
- if not text:
- continue
- if role == "title":
- html_parts.append(f"")
- else:
- html_parts.append(f"{text}
")
- html_parts.append(" ")
- elif canonical_text:
- html_parts.append("")
- paragraphs = [
- p.strip()
- for p in canonical_text.replace("\r\n", "\n").split("\n\n")
- if p.strip()
- ]
- for p in paragraphs:
- html_parts.append(f"{p}
")
- html_parts.append(" ")
-
- # Aufgaben
- if tasks:
- html_parts.append("")
- html_parts.append("Aufgaben ")
-
- for idx, task in enumerate(tasks, start=1):
- t_type = task.get("type") or "Aufgabe"
- desc = task.get("description") or ""
- text_with_gaps = task.get("text_with_gaps")
-
- html_parts.append("")
-
- # Task-Header
- type_label = {
- "fill_in_blank": "Lückentext",
- "multiple_choice": "Multiple Choice",
- "free_text": "Freitext",
- "matching": "Zuordnung",
- "labeling": "Beschriftung",
- "calculation": "Rechnung",
- "other": "Aufgabe"
- }.get(t_type, t_type)
-
- html_parts.append(f"")
-
- if desc:
- html_parts.append(f"
{desc}
")
-
- if text_with_gaps:
- rendered = text_with_gaps.replace("___", "
")
- html_parts.append(f"
{rendered}
")
-
- # Antwortlinien für Freitext-Aufgaben
- if t_type in ["free_text", "other"] or (not text_with_gaps and not desc):
- html_parts.append("
")
- for _ in range(3):
- html_parts.append("
")
- html_parts.append("
")
-
- html_parts.append("
")
-
- html_parts.append(" ")
-
- # Fußzeile
- html_parts.append("")
-
- html_parts.append("")
-
- return "\n".join(html_parts)
+from .print_qa import generate_print_version_qa
+from .print_cloze import generate_print_version_cloze
+from .print_mc import generate_print_version_mc
+from .print_worksheet import generate_print_version_worksheet
+
+__all__ = [
+ "generate_print_version_qa",
+ "generate_print_version_cloze",
+ "generate_print_version_mc",
+ "generate_print_version_worksheet",
+]
diff --git a/backend-lehrer/ai_processing/print_mc.py b/backend-lehrer/ai_processing/print_mc.py
new file mode 100644
index 0000000..57cf2b1
--- /dev/null
+++ b/backend-lehrer/ai_processing/print_mc.py
@@ -0,0 +1,240 @@
+"""
+AI Processing - Print Version Generator: Multiple Choice.
+
+Generates printable HTML for multiple-choice worksheets.
+"""
+
+from pathlib import Path
+import json
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def generate_print_version_mc(mc_path: Path, include_answers: bool = False) -> str:
+ """
+ Generiert eine druckbare HTML-Version der Multiple-Choice-Fragen.
+
+ Args:
+ mc_path: Pfad zur *_mc.json Datei
+ include_answers: True fuer Loesungsblatt mit markierten richtigen Antworten
+
+ Returns:
+ HTML-String (zum direkten Ausliefern)
+ """
+ if not mc_path.exists():
+ raise FileNotFoundError(f"MC-Datei nicht gefunden: {mc_path}")
+
+ mc_data = json.loads(mc_path.read_text(encoding="utf-8"))
+ questions = mc_data.get("questions", [])
+ metadata = mc_data.get("metadata", {})
+
+ title = metadata.get("source_title", "Arbeitsblatt")
+ subject = metadata.get("subject", "")
+ grade = metadata.get("grade_level", "")
+
+ html_parts = []
+ html_parts.append("""
+
+
+
+""" + title + """ - Multiple Choice
+
+
+
+""")
+
+ # Header
+ version_text = "Loesungsblatt" if include_answers else "Multiple Choice Test"
+ html_parts.append(f"{title} ")
+ html_parts.append(f"{version_text} ")
+ if subject:
+ html_parts.append(f" | Fach: {subject}")
+ if grade:
+ html_parts.append(f" | Klasse: {grade}")
+ html_parts.append(f" | Anzahl Fragen: {len(questions)}
")
+
+ if not include_answers:
+ html_parts.append("")
+ html_parts.append("Anleitung: Kreuze bei jeder Frage die richtige Antwort an. ")
+ html_parts.append("Es ist immer nur eine Antwort richtig.")
+ html_parts.append("
")
+
+ # Fragen
+ for idx, q in enumerate(questions, 1):
+ html_parts.append("")
+ html_parts.append(f"
Frage {idx}
")
+ html_parts.append(f"
{q.get('question', '')}
")
+
+ html_parts.append("
")
+ correct_answer = q.get("correct_answer", "")
+
+ for opt in q.get("options", []):
+ opt_id = opt.get("id", "")
+ is_correct = opt_id == correct_answer
+
+ opt_class = "option"
+ checkbox_class = "option-checkbox"
+ if include_answers and is_correct:
+ opt_class += " option-correct"
+ checkbox_class += " checked"
+
+ html_parts.append(f"
")
+ html_parts.append(f"
")
+ html_parts.append(f"
{opt_id}) ")
+ html_parts.append(f"
{opt.get('text', '')} ")
+ html_parts.append("
")
+
+ html_parts.append("
")
+
+ # Erklaerung nur bei Loesungsblatt
+ if include_answers and q.get("explanation"):
+ html_parts.append(f"
Erklaerung: {q.get('explanation')}
")
+
+ html_parts.append("
")
+
+ # Loesungsschluessel (kompakt) - nur bei Loesungsblatt
+ if include_answers:
+ html_parts.append("")
+ html_parts.append("
Loesungsschluessel
")
+ html_parts.append("
")
+ for idx, q in enumerate(questions, 1):
+ html_parts.append("
")
+ html_parts.append(f"{idx}. ")
+ html_parts.append(f"{q.get('correct_answer', '')} ")
+ html_parts.append("
")
+ html_parts.append("
")
+ html_parts.append("
")
+
+ html_parts.append("")
+
+ return "\n".join(html_parts)
diff --git a/backend-lehrer/ai_processing/print_qa.py b/backend-lehrer/ai_processing/print_qa.py
new file mode 100644
index 0000000..0e75c9f
--- /dev/null
+++ b/backend-lehrer/ai_processing/print_qa.py
@@ -0,0 +1,149 @@
+"""
+AI Processing - Print Version Generator: Q&A.
+
+Generates printable HTML for question-answer worksheets.
+"""
+
+from pathlib import Path
+import json
+import logging
+
+from .core import BEREINIGT_DIR
+
+logger = logging.getLogger(__name__)
+
+
+def generate_print_version_qa(qa_path: Path, include_answers: bool = False) -> Path:
+ """
+ Generiert eine druckbare HTML-Version der Frage-Antwort-Paare.
+
+ Args:
+ qa_path: Pfad zur *_qa.json Datei
+ include_answers: True fuer Loesungsblatt (fuer Eltern)
+
+ Returns:
+ Pfad zur generierten HTML-Datei
+ """
+ if not qa_path.exists():
+ raise FileNotFoundError(f"Q&A-Datei nicht gefunden: {qa_path}")
+
+ qa_data = json.loads(qa_path.read_text(encoding="utf-8"))
+ items = qa_data.get("qa_items", [])
+ metadata = qa_data.get("metadata", {})
+
+ title = metadata.get("source_title", "Arbeitsblatt")
+ subject = metadata.get("subject", "")
+ grade = metadata.get("grade_level", "")
+
+ html_parts = []
+ html_parts.append("""
+
+
+
+""" + title + """ - Fragen
+
+
+
+""")
+
+ # Header
+ version_text = "Loesungsblatt" if include_answers else "Fragenblatt"
+ html_parts.append(f"{title} - {version_text} ")
+ meta_parts = []
+ if subject:
+ meta_parts.append(f"Fach: {subject}")
+ if grade:
+ meta_parts.append(f"Klasse: {grade}")
+ meta_parts.append(f"Anzahl Fragen: {len(items)}")
+ html_parts.append(f"{' | '.join(meta_parts)}
")
+
+ # Fragen
+ for idx, item in enumerate(items, 1):
+ html_parts.append("")
+ html_parts.append(f"
Frage {idx}
")
+ html_parts.append(f"
{item.get('question', '')}
")
+
+ if include_answers:
+ # Loesungsblatt: Antwort anzeigen
+ html_parts.append(f"
Antwort: {item.get('answer', '')}
")
+ # Schluesselbegriffe
+ key_terms = item.get("key_terms", [])
+ if key_terms:
+ terms_html = " ".join([f"
{term} " for term in key_terms])
+ html_parts.append(f"
Wichtige Begriffe: {terms_html}
")
+ else:
+ # Fragenblatt: Antwortlinien
+ html_parts.append("
")
+ for _ in range(3):
+ html_parts.append("
")
+ html_parts.append("
")
+
+ html_parts.append("
")
+
+ html_parts.append("")
+
+ # Speichern
+ suffix = "_qa_solutions.html" if include_answers else "_qa_print.html"
+ out_name = qa_path.stem.replace("_qa", "") + suffix
+ out_path = BEREINIGT_DIR / out_name
+ out_path.write_text("\n".join(html_parts), encoding="utf-8")
+
+ logger.info(f"Print-Version gespeichert: {out_path.name}")
+ return out_path
diff --git a/backend-lehrer/ai_processing/print_worksheet.py b/backend-lehrer/ai_processing/print_worksheet.py
new file mode 100644
index 0000000..131e84d
--- /dev/null
+++ b/backend-lehrer/ai_processing/print_worksheet.py
@@ -0,0 +1,294 @@
+"""
+AI Processing - Print Version Generator: Worksheet.
+
+Generates print-optimized HTML for general worksheets from analysis data.
+"""
+
+from pathlib import Path
+import json
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def generate_print_version_worksheet(analysis_path: Path) -> str:
+ """
+ Generiert eine druckoptimierte HTML-Version des Arbeitsblatts.
+
+ Eigenschaften:
+ - Grosse, gut lesbare Schrift (16pt)
+ - Schwarz-weiss / Graustufen-tauglich
+ - Klare Struktur fuer Druck
+ - Keine interaktiven Elemente
+
+ Args:
+ analysis_path: Pfad zur *_analyse.json Datei
+
+ Returns:
+ HTML-String zum direkten Ausliefern
+ """
+ if not analysis_path.exists():
+ raise FileNotFoundError(f"Analysedatei nicht gefunden: {analysis_path}")
+
+ try:
+ data = json.loads(analysis_path.read_text(encoding="utf-8"))
+ except json.JSONDecodeError as e:
+ raise RuntimeError(f"Analyse-Datei enthaelt kein gueltiges JSON: {analysis_path}\n{e}") from e
+
+ title = data.get("title") or "Arbeitsblatt"
+ subject = data.get("subject") or ""
+ grade_level = data.get("grade_level") or ""
+ instructions = data.get("instructions") or ""
+ tasks = data.get("tasks", []) or []
+ canonical_text = data.get("canonical_text") or ""
+ printed_blocks = data.get("printed_blocks") or []
+
+ html_parts = []
+ html_parts.append(_build_html_head(title))
+
+ # Titel
+ html_parts.append(f"{title} ")
+
+ # Meta-Informationen
+ meta_parts = []
+ if subject:
+ meta_parts.append(f"Fach: {subject} ")
+ if grade_level:
+ meta_parts.append(f"Klasse: {grade_level} ")
+ if meta_parts:
+ html_parts.append(f"{''.join(meta_parts)}
")
+
+ # Arbeitsanweisung
+ if instructions:
+ html_parts.append("")
+ html_parts.append("
Arbeitsanweisung:
")
+ html_parts.append(f"
{instructions}
")
+ html_parts.append("
")
+
+ # Haupttext / gedruckte Bloecke
+ _build_text_section(html_parts, printed_blocks, canonical_text)
+
+ # Aufgaben
+ _build_tasks_section(html_parts, tasks)
+
+ # Fusszeile
+ html_parts.append("")
+
+ html_parts.append("