Files
breakpilot-lehrer/backend-lehrer/ai_processing/cloze_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

329 lines
10 KiB
Python

"""
AI Processing - Cloze/Lückentext Generator.
Generiert Lückentexte mit Übersetzungen aus Arbeitsblatt-Analysen.
"""
from pathlib import Path
import json
import os
import requests
import logging
from .core import (
get_openai_api_key,
get_vision_api,
BEREINIGT_DIR,
)
logger = logging.getLogger(__name__)
# Sprachcodes zu Namen
LANGUAGE_NAMES = {
"tr": "Türkisch",
"ar": "Arabisch",
"ru": "Russisch",
"en": "Englisch",
"fr": "Französisch",
"es": "Spanisch",
"pl": "Polnisch",
"uk": "Ukrainisch",
}
def _generate_cloze_with_openai(analysis_data: dict, target_language: str = "tr") -> dict:
"""
Generiert Lückentexte basierend auf der Arbeitsblatt-Analyse.
Wichtige didaktische Anforderungen:
- Mehrere sinnvolle Lücken pro Satz (nicht nur eine!)
- Schwierigkeitsgrad entspricht dem Original
- Übersetzung mit denselben Lücken
Args:
analysis_data: Die Analyse-JSON des Arbeitsblatts
target_language: Zielsprache für Übersetzung (default: "tr" für Türkisch)
Returns:
Dict mit cloze_items und metadata
"""
api_key = get_openai_api_key()
# Extrahiere relevante Inhalte
title = analysis_data.get("title") or "Arbeitsblatt"
subject = analysis_data.get("subject") or "Allgemein"
grade_level = analysis_data.get("grade_level") or "unbekannt"
canonical_text = analysis_data.get("canonical_text") or ""
printed_blocks = analysis_data.get("printed_blocks") or []
# Baue Textinhalt zusammen
content_parts = []
if canonical_text:
content_parts.append(canonical_text)
for block in printed_blocks:
text = block.get("text", "").strip()
if text and text not in content_parts:
content_parts.append(text)
worksheet_content = "\n\n".join(content_parts)
if not worksheet_content.strip():
logger.warning("Kein Textinhalt für Lückentext-Generierung gefunden")
return {"cloze_items": [], "metadata": {"error": "Kein Textinhalt gefunden"}}
target_lang_name = LANGUAGE_NAMES.get(target_language, "Türkisch")
url = "https://api.openai.com/v1/chat/completions"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
system_prompt = f"""Du bist ein erfahrener Pädagoge, der Lückentexte für Schüler erstellt.
WICHTIGE REGELN FÜR LÜCKENTEXTE:
1. MEHRERE LÜCKEN PRO SATZ:
- Erstelle IMMER mehrere sinnvolle Lücken pro Satz
- Beispiel: "Ich habe gestern meine Hausaufgaben gemacht."
→ Lücken: "habe" UND "gemacht" (nicht nur eine!)
- Wähle Wörter, die für das Verständnis wichtig sind
2. SCHWIERIGKEITSGRAD:
- Niveau muss exakt "{grade_level}" entsprechen
- Nicht zu leicht, nicht zu schwer
- Altersgerechte Lücken wählen
3. SINNVOLLE LÜCKENWÖRTER:
- Verben (konjugiert)
- Wichtige Nomen
- Adjektive
- KEINE Artikel oder Präpositionen allein
4. ÜBERSETZUNG:
- Übersetze den VOLLSTÄNDIGEN Satz auf {target_lang_name}
- Die GLEICHEN Wörter müssen als Lücken markiert sein
- Die Übersetzung dient als Hilfe für Eltern
5. AUSGABE: Nur gültiges JSON, kein Markdown."""
user_prompt = f"""Erstelle Lückentexte aus diesem Arbeitsblatt:
TITEL: {title}
FACH: {subject}
KLASSENSTUFE: {grade_level}
TEXT:
{worksheet_content}
Erstelle 5-8 Sätze mit Lücken. Gib das Ergebnis als JSON zurück:
{{
"cloze_items": [
{{
"id": "c1",
"original_sentence": "Der vollständige Originalsatz ohne Lücken",
"sentence_with_gaps": "Der Satz mit ___ für jede Lücke",
"gaps": [
{{
"id": "g1",
"word": "das fehlende Wort",
"position": 0,
"hint": "optionaler Hinweis"
}}
],
"translation": {{
"language": "{target_language}",
"language_name": "{target_lang_name}",
"full_sentence": "Vollständige Übersetzung",
"sentence_with_gaps": "Übersetzung mit ___ an gleichen Stellen"
}}
}}
],
"metadata": {{
"subject": "{subject}",
"grade_level": "{grade_level}",
"source_title": "{title}",
"target_language": "{target_language}",
"total_gaps": 0
}}
}}
WICHTIG:
- Jeder Satz MUSS mindestens 2 Lücken haben!
- Die Lücken in der Übersetzung müssen den deutschen Lücken entsprechen
- Position ist der Index des Wortes im Satz (0-basiert)"""
payload = {
"model": "gpt-4o-mini",
"response_format": {"type": "json_object"},
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"max_tokens": 3000,
"temperature": 0.7,
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
try:
content = data["choices"][0]["message"]["content"]
cloze_data = json.loads(content)
except (KeyError, json.JSONDecodeError) as e:
raise RuntimeError(f"Fehler bei Lückentext-Generierung: {e}")
# Berechne Gesamtzahl der Lücken
total_gaps = sum(len(item.get("gaps", [])) for item in cloze_data.get("cloze_items", []))
if "metadata" in cloze_data:
cloze_data["metadata"]["total_gaps"] = total_gaps
return cloze_data
def _generate_cloze_with_claude(analysis_data: dict, target_language: str = "tr") -> dict:
"""
Generiert Lückentexte mit Claude API.
"""
import anthropic
api_key = os.getenv("ANTHROPIC_API_KEY")
if not api_key:
raise RuntimeError("ANTHROPIC_API_KEY ist nicht gesetzt.")
client = anthropic.Anthropic(api_key=api_key)
# Extrahiere relevante Inhalte
title = analysis_data.get("title") or "Arbeitsblatt"
subject = analysis_data.get("subject") or "Allgemein"
grade_level = analysis_data.get("grade_level") or "unbekannt"
canonical_text = analysis_data.get("canonical_text") or ""
printed_blocks = analysis_data.get("printed_blocks") or []
content_parts = []
if canonical_text:
content_parts.append(canonical_text)
for block in printed_blocks:
text = block.get("text", "").strip()
if text and text not in content_parts:
content_parts.append(text)
worksheet_content = "\n\n".join(content_parts)
if not worksheet_content.strip():
return {"cloze_items": [], "metadata": {"error": "Kein Textinhalt gefunden"}}
target_lang_name = LANGUAGE_NAMES.get(target_language, "Türkisch")
prompt = f"""Erstelle Lückentexte aus diesem Arbeitsblatt.
WICHTIGE REGELN:
1. MEHRERE LÜCKEN PRO SATZ (mindestens 2!)
Beispiel: "Ich habe gestern Hausaufgaben gemacht" → Lücken: "habe" UND "gemacht"
2. Schwierigkeitsgrad: exakt "{grade_level}"
3. Übersetzung auf {target_lang_name} mit gleichen Lücken
TITEL: {title}
FACH: {subject}
KLASSENSTUFE: {grade_level}
TEXT:
{worksheet_content}
Antworte NUR mit diesem JSON (5-8 Sätze):
{{
"cloze_items": [
{{
"id": "c1",
"original_sentence": "Vollständiger Satz",
"sentence_with_gaps": "Satz mit ___ für Lücken",
"gaps": [
{{"id": "g1", "word": "Lückenwort", "position": 0, "hint": "Hinweis"}}
],
"translation": {{
"language": "{target_language}",
"language_name": "{target_lang_name}",
"full_sentence": "Übersetzung",
"sentence_with_gaps": "Übersetzung mit ___"
}}
}}
],
"metadata": {{
"subject": "{subject}",
"grade_level": "{grade_level}",
"source_title": "{title}",
"target_language": "{target_language}",
"total_gaps": 0
}}
}}"""
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=3000,
messages=[{"role": "user", "content": prompt}]
)
content = message.content[0].text
try:
if "```json" in content:
content = content.split("```json")[1].split("```")[0]
elif "```" in content:
content = content.split("```")[1].split("```")[0]
cloze_data = json.loads(content.strip())
except json.JSONDecodeError as e:
raise RuntimeError(f"Claude hat ungültiges JSON geliefert: {e}")
# Berechne Gesamtzahl der Lücken
total_gaps = sum(len(item.get("gaps", [])) for item in cloze_data.get("cloze_items", []))
if "metadata" in cloze_data:
cloze_data["metadata"]["total_gaps"] = total_gaps
return cloze_data
def generate_cloze_from_analysis(analysis_path: Path, target_language: str = "tr") -> Path:
"""
Generiert Lückentexte aus einer Analyse-JSON-Datei.
Die Lückentexte werden:
- Mit mehreren sinnvollen Lücken pro Satz erstellt
- Auf dem Schwierigkeitsniveau des Originals gehalten
- Mit Übersetzung in die Zielsprache versehen
Args:
analysis_path: Pfad zur *_analyse.json Datei
target_language: Sprachcode für Übersetzung (default: "tr" für Türkisch)
Returns:
Pfad zur generierten *_cloze.json Datei
"""
if not analysis_path.exists():
raise FileNotFoundError(f"Analysedatei nicht gefunden: {analysis_path}")
try:
analysis_data = json.loads(analysis_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
raise RuntimeError(f"Ungültige Analyse-JSON: {e}")
logger.info(f"Generiere Lückentexte für: {analysis_path.name}")
vision_api = get_vision_api()
# Generiere Lückentexte (nutze konfigurierte API)
if vision_api == "claude":
try:
cloze_data = _generate_cloze_with_claude(analysis_data, target_language)
except Exception as e:
logger.warning(f"Claude Lückentext-Generierung fehlgeschlagen, nutze OpenAI: {e}")
cloze_data = _generate_cloze_with_openai(analysis_data, target_language)
else:
cloze_data = _generate_cloze_with_openai(analysis_data, target_language)
# Speichere Lückentext-Daten
out_name = analysis_path.stem.replace("_analyse", "") + "_cloze.json"
out_path = BEREINIGT_DIR / out_name
out_path.write_text(json.dumps(cloze_data, ensure_ascii=False, indent=2), encoding="utf-8")
logger.info(f"Lückentexte gespeichert: {out_path.name}")
return out_path