""" AI Processing - Mindmap Generator. Generiert kindgerechte Lernposter-Mindmaps aus Arbeitsblatt-Analysen. """ from pathlib import Path import math import json import os import requests import logging from .core import get_openai_api_key, BEREINIGT_DIR logger = logging.getLogger(__name__) def generate_mindmap_data(analysis_path: Path) -> dict: """ Extrahiert Fachbegriffe aus der Analyse und gruppiert sie für eine Mindmap. Args: analysis_path: Pfad zur *_analyse.json Datei Returns: Dictionary mit Mindmap-Struktur: { "topic": "Hauptthema", "subject": "Fach", "categories": [ { "name": "Kategorie", "color": "#hexcolor", "emoji": "🔬", "terms": [ {"term": "Begriff", "explanation": "Kurze Erklärung"} ] } ] } """ 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 "" canonical_text = data.get("canonical_text") or "" tasks = data.get("tasks", []) or [] # Sammle allen Text für die Analyse all_text = canonical_text for task in tasks: if task.get("description"): all_text += "\n" + task.get("description") if task.get("text_with_gaps"): all_text += "\n" + task.get("text_with_gaps") if not all_text.strip(): return { "topic": title, "subject": subject, "categories": [] } # KI-basierte Extraktion der Fachbegriffe api_key = get_openai_api_key() prompt = f"""Analysiere diesen Schultext und extrahiere alle Fachbegriffe für eine kindgerechte Lern-Mindmap. THEMA: {title} FACH: {subject} TEXT: {all_text[:3000]} AUFGABE: 1. Identifiziere das Hauptthema (ein einzelnes Wort oder kurzer Begriff) 2. Finde ALLE Fachbegriffe und gruppiere sie in 3-6 sinnvolle Kategorien 3. Gib für jeden Begriff eine kurze, kindgerechte Erklärung (max 10 Wörter) 4. Wähle für jede Kategorie ein passendes Emoji und eine Farbe Antworte NUR mit diesem JSON-Format: {{ "topic": "Hauptthema (z.B. 'Das Auge')", "categories": [ {{ "name": "Kategoriename", "emoji": "passendes Emoji", "color": "#Hexfarbe (bunt, kindgerecht)", "terms": [ {{"term": "Fachbegriff", "explanation": "Kurze Erklärung"}} ] }} ] }} WICHTIG: - Verwende kindgerechte, einfache Sprache - Bunte, fröhliche Farben: #FF6B6B, #4ECDC4, #45B7D1, #96CEB4, #FFEAA7, #DDA0DD, #98D8C8 - Passende Emojis für jede Kategorie - Mindestens 3 Begriffe pro Kategorie wenn möglich - Maximal 6 Kategorien""" try: # Versuche Claude claude_key = os.environ.get("ANTHROPIC_API_KEY") if claude_key: import anthropic client = anthropic.Anthropic(api_key=claude_key) response = client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=2000, messages=[{"role": "user", "content": prompt}] ) result_text = response.content[0].text else: # Fallback zu OpenAI logger.info("Claude Mindmap-Generierung fehlgeschlagen, nutze OpenAI: ANTHROPIC_API_KEY ist nicht gesetzt.") url = "https://api.openai.com/v1/chat/completions" headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} payload = { "model": "gpt-4o-mini", "messages": [ {"role": "system", "content": "Du bist ein Experte für kindgerechte Lernmaterialien."}, {"role": "user", "content": prompt} ], "max_tokens": 2000, "temperature": 0.7 } resp = requests.post(url, headers=headers, json=payload, timeout=60) resp.raise_for_status() result_text = resp.json()["choices"][0]["message"]["content"] # JSON extrahieren result_text = result_text.strip() if result_text.startswith("```"): result_text = result_text.split("```")[1] if result_text.startswith("json"): result_text = result_text[4:] result_text = result_text.strip() mindmap_data = json.loads(result_text) mindmap_data["subject"] = subject return mindmap_data except Exception as e: logger.error(f"Mindmap-Generierung fehlgeschlagen: {e}") # Fallback: Einfache Struktur zurückgeben return { "topic": title, "subject": subject, "categories": [] } def generate_mindmap_html(mindmap_data: dict, format: str = "a3") -> str: """ Generiert ein kindgerechtes HTML/SVG Mindmap-Poster. Args: mindmap_data: Dictionary aus generate_mindmap_data() format: "a3" für A3-Poster (Standard) oder "a4" für A4-Ansicht Returns: HTML-String mit SVG-Mindmap """ topic = mindmap_data.get("topic", "Thema") subject = mindmap_data.get("subject", "") categories = mindmap_data.get("categories", []) # Format-spezifische Einstellungen if format.lower() == "a4": page_size = "A4 landscape" svg_width = 1100 svg_height = 780 radius = 250 else: # a3 (Standard) page_size = "A3 landscape" svg_width = 1400 svg_height = 990 radius = 320 # Wenn keine Kategorien, zeige Platzhalter if not categories: return f""" Mindmap - {topic}

🧠 Mindmap: {topic}

Noch keine Daten vorhanden. Bitte zuerst das Arbeitsblatt analysieren.

""" # Farben für Verbindungslinien num_categories = len(categories) # SVG-Dimensionen wurden oben basierend auf format gesetzt center_x = svg_width // 2 center_y = svg_height // 2 # Berechne Positionen der Kategorien im Kreis category_positions = [] for i, cat in enumerate(categories): angle = (2 * math.pi * i / num_categories) - (math.pi / 2) # Start oben x = center_x + radius * math.cos(angle) y = center_y + radius * math.sin(angle) category_positions.append({ "x": x, "y": y, "angle": angle, "data": cat }) html = f""" Lernposter - {topic}
🧠 Lernposter: {topic}
{subject}
""" # Zeichne Verbindungslinien for pos in category_positions: color = pos["data"].get("color", "#4ECDC4") html += f""" """ # Zentrum (Hauptthema) html += f""" 🌟 {topic} """ # Zeichne Kategorien mit ihren Begriffen for i, pos in enumerate(category_positions): cat = pos["data"] cat_x = pos["x"] cat_y = pos["y"] color = cat.get("color", "#4ECDC4") emoji = cat.get("emoji", "📚") name = cat.get("name", "Kategorie") terms = cat.get("terms", []) # Kategorie-Bubble html += f""" {emoji} {name} """ # Begriffe um die Kategorie herum term_radius = 110 num_terms = len(terms) for j, term_data in enumerate(terms[:8]): # Max 8 Begriffe pro Kategorie term = term_data.get("term", "") # Berechne Position relativ zur Kategorie # Verteile Begriffe in einem Halbkreis auf der Außenseite base_angle = pos["angle"] spread = math.pi * 0.8 # 80% eines Halbkreises if num_terms > 1: term_angle = base_angle - spread/2 + (spread * j / (num_terms - 1)) else: term_angle = base_angle term_x = term_radius * math.cos(term_angle - base_angle) term_y = term_radius * math.sin(term_angle - base_angle) # Kleine Verbindungslinie html += f""" """ # Begriff-Bubble bubble_width = max(70, len(term) * 8 + 20) html += f""" {term} """ html += " \n" # Legende mit Erklärungen (unten) html += f""" 📖 Begriffe zum Lernen: """ legend_x = 0 for i, pos in enumerate(category_positions): cat = pos["data"] color = cat.get("color", "#4ECDC4") emoji = cat.get("emoji", "📚") name = cat.get("name", "") terms = cat.get("terms", []) # Zeige Kategorie mit ersten 3 Begriffen terms_text = ", ".join([t.get("term", "") for t in terms[:3]]) if len(terms) > 3: terms_text += "..." html += f""" {emoji} {name}: {terms_text} """ legend_x += 220 html += """
""" return html def save_mindmap_for_worksheet(analysis_path: Path, mindmap_data: dict = None) -> Path: """ Speichert eine Mindmap für ein Arbeitsblatt. Args: analysis_path: Pfad zur *_analyse.json Datei mindmap_data: Optional - bereits generierte Mindmap-Daten. Falls nicht angegeben, werden sie generiert. Returns: Pfad zur gespeicherten *_mindmap.json Datei """ if mindmap_data is None: mindmap_data = generate_mindmap_data(analysis_path) # Speichere JSON out_name = analysis_path.stem.replace("_analyse", "") + "_mindmap.json" out_path = BEREINIGT_DIR / out_name out_path.write_text(json.dumps(mindmap_data, ensure_ascii=False, indent=2), encoding="utf-8") logger.info(f"Mindmap-Daten gespeichert: {out_path.name}") return out_path