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