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/ai_processing/mindmap.py
Benjamin Admin 21a844cb8a 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

473 lines
15 KiB
Python

"""
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"""<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Mindmap - {topic}</title>
<style>
body {{ font-family: 'Comic Sans MS', cursive, sans-serif; text-align: center; padding: 50px; }}
h1 {{ color: #FF6B6B; }}
</style>
</head>
<body>
<h1>🧠 Mindmap: {topic}</h1>
<p>Noch keine Daten vorhanden. Bitte zuerst das Arbeitsblatt analysieren.</p>
</body>
</html>"""
# 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"""<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Lernposter - {topic}</title>
<style>
@page {{
size: {page_size};
margin: 10mm;
}}
@media print {{
body {{ -webkit-print-color-adjust: exact; print-color-adjust: exact; }}
.no-print {{ display: none !important; }}
}}
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{
font-family: 'Comic Sans MS', 'Chalkboard SE', 'Comic Neue', cursive, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
min-height: 100vh;
padding: 20px;
}}
.poster-container {{
width: 100%;
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}}
.poster-header {{
background: linear-gradient(90deg, #FF6B6B, #4ECDC4);
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}}
.poster-title {{
color: white;
font-size: 24px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}}
.poster-subject {{
color: white;
font-size: 16px;
opacity: 0.9;
}}
.mindmap-svg {{
width: 100%;
height: auto;
}}
.print-btn {{
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
background: #4ECDC4;
color: white;
border: none;
border-radius: 25px;
font-size: 16px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(78, 205, 196, 0.4);
font-family: inherit;
}}
.print-btn:hover {{
transform: scale(1.05);
background: #45B7D1;
}}
/* Animationen für interaktive Version */
.category-group:hover {{
transform: scale(1.02);
cursor: pointer;
}}
.term-bubble:hover {{
transform: scale(1.1);
filter: brightness(1.1);
}}
</style>
</head>
<body>
<button class="print-btn no-print" onclick="window.print()">🖨️ Als A3 drucken</button>
<div class="poster-container">
<div class="poster-header">
<div class="poster-title">🧠 Lernposter: {topic}</div>
<div class="poster-subject">{subject}</div>
</div>
<svg class="mindmap-svg" viewBox="0 0 {svg_width} {svg_height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Schatten für Bubbles -->
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="2" dy="4" stdDeviation="4" flood-opacity="0.2"/>
</filter>
<!-- Glow-Effekt für Zentrum -->
<filter id="glow">
<feGaussianBlur stdDeviation="8" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Hintergrund-Muster (dezente Punkte) -->
<pattern id="dots" x="0" y="0" width="30" height="30" patternUnits="userSpaceOnUse">
<circle cx="15" cy="15" r="1.5" fill="#e0e0e0"/>
</pattern>
<rect width="100%" height="100%" fill="url(#dots)"/>
<!-- Verbindungslinien vom Zentrum zu Kategorien -->
"""
# Zeichne Verbindungslinien
for pos in category_positions:
color = pos["data"].get("color", "#4ECDC4")
html += f""" <path d="M {center_x} {center_y} Q {(center_x + pos['x'])/2 + 30} {(center_y + pos['y'])/2 - 30} {pos['x']} {pos['y']}"
stroke="{color}" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.6"/>
"""
# Zentrum (Hauptthema)
html += f"""
<!-- Zentrum: Hauptthema -->
<g filter="url(#glow)">
<circle cx="{center_x}" cy="{center_y}" r="85" fill="url(#centerGradient)"/>
<defs>
<radialGradient id="centerGradient" cx="30%" cy="30%">
<stop offset="0%" stop-color="#FFD93D"/>
<stop offset="100%" stop-color="#FF6B6B"/>
</radialGradient>
</defs>
<text x="{center_x}" y="{center_y - 10}" text-anchor="middle" font-size="28" font-weight="bold" fill="white">🌟</text>
<text x="{center_x}" y="{center_y + 25}" text-anchor="middle" font-size="22" font-weight="bold" fill="white">{topic}</text>
</g>
"""
# 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"""
<!-- Kategorie: {name} -->
<g class="category-group" transform="translate({cat_x}, {cat_y})">
<ellipse cx="0" cy="0" rx="75" ry="45" fill="{color}" filter="url(#shadow)"/>
<text x="0" y="-8" text-anchor="middle" font-size="20">{emoji}</text>
<text x="0" y="18" text-anchor="middle" font-size="14" font-weight="bold" fill="white">{name}</text>
"""
# 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""" <line x1="0" y1="0" x2="{term_x * 0.6}" y2="{term_y * 0.6}" stroke="{color}" stroke-width="2" opacity="0.5"/>
"""
# Begriff-Bubble
bubble_width = max(70, len(term) * 8 + 20)
html += f""" <g class="term-bubble" transform="translate({term_x}, {term_y})">
<rect x="{-bubble_width/2}" y="-22" width="{bubble_width}" height="44" rx="22" fill="white" stroke="{color}" stroke-width="2" filter="url(#shadow)"/>
<text x="0" y="5" text-anchor="middle" font-size="12" font-weight="bold" fill="#333">{term}</text>
</g>
"""
html += " </g>\n"
# Legende mit Erklärungen (unten)
html += f"""
<!-- Legende -->
<g transform="translate(50, {svg_height - 80})">
<text x="0" y="0" font-size="14" font-weight="bold" fill="#666">📖 Begriffe zum Lernen:</text>
"""
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""" <g transform="translate({legend_x}, 25)">
<circle cx="8" cy="0" r="8" fill="{color}"/>
<text x="22" y="4" font-size="11" fill="#444"><tspan font-weight="bold">{emoji} {name}:</tspan> {terms_text}</text>
</g>
"""
legend_x += 220
html += """ </g>
</svg>
</div>
</body>
</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