from datetime import date from typing import List from fastapi import APIRouter, UploadFile, File from fastapi.responses import HTMLResponse import shutil from config import ( BASE_DIR, EINGANG_DIR, BEREINIGT_DIR, EDITIERBAR_DIR, NEU_GENERIERT_DIR, is_valid_input_file, ) from ai_processor import ( dummy_process_scan, describe_scan_with_ai, analyze_scan_structure_with_ai, build_clean_html_from_analysis, remove_handwriting_from_scan, generate_mc_from_analysis, generate_cloze_from_analysis, generate_qa_from_analysis, update_leitner_progress, get_next_review_items, generate_print_version_qa, generate_print_version_cloze, generate_print_version_mc, generate_print_version_worksheet, generate_mindmap_data, generate_mindmap_html, save_mindmap_for_worksheet, ) router = APIRouter() @router.get("/") def home(): """Basis-Info des Backends (unter /api/ erreichbar).""" return { "status": "OK", "message": "BreakPilot Backend läuft.", "base_dir": str(BASE_DIR), } # === UPLOAD === @router.post("/upload-scan") async def upload_scan(file: UploadFile = File(...)): """ Einzelnen Scan hochladen. Dateiname bekommt automatisch ein Datum vorne dran: YYYY-MM-DD_originalname.ext """ today = date.today().isoformat() safe_name = file.filename.replace("/", "_") target_name = f"{today}_{safe_name}" target_path = EINGANG_DIR / target_name with target_path.open("wb") as buffer: shutil.copyfileobj(file.file, buffer) return { "status": "OK", "message": "Scan gespeichert", "saved_as": str(target_path), } @router.get("/upload-form", response_class=HTMLResponse) def upload_form(): """Einfache HTML-Upload-Seite (für schnelle Tests im Browser).""" return """

Arbeitsblätter hochladen

""" @router.post("/upload-multi") async def upload_multi(files: List[UploadFile] = File(...)): """ Mehrere Dateien per Stapel hochladen. Alle Dateien landen im Ordner Eingang mit Datumspräfix. """ today = date.today().isoformat() saved = [] for file in files: safe_name = file.filename.replace("/", "_") target_name = f"{today}_{safe_name}" target_path = EINGANG_DIR / target_name with target_path.open("wb") as buffer: shutil.copyfileobj(file.file, buffer) saved.append(str(target_path)) return {"status": "OK", "message": "Dateien gespeichert", "saved_as": saved} # === LISTEN & LÖSCHEN === @router.get("/eingang-dateien") def list_eingang_files(): files = [f.name for f in EINGANG_DIR.iterdir() if is_valid_input_file(f)] return {"eingang": files} @router.delete("/eingang-dateien/{filename}") def delete_eingang_file(filename: str): """ Löscht eine Datei komplett und entfernt sie aus allen Lerneinheiten. """ from learning_units import get_all_units, update_unit path = EINGANG_DIR / filename if not path.exists() or not path.is_file(): return {"status": "ERROR", "message": "Datei nicht gefunden", "filename": filename} # Datei aus allen Lerneinheiten entfernen units = get_all_units() units_updated = 0 for unit in units: if filename in unit.get("worksheet_files", []): # Datei aus der Liste entfernen unit["worksheet_files"].remove(filename) # Lerneinheit aktualisieren update_unit(unit["id"], unit) units_updated += 1 # Physische Datei löschen path.unlink() return { "status": "OK", "message": "Datei gelöscht", "filename": filename, "units_updated": units_updated } @router.get("/bereinigt-dateien") def list_bereinigt_files(): files = [f.name for f in BEREINIGT_DIR.iterdir() if f.is_file()] return {"bereinigt": files} # === VERARBEITUNG: Dummy / Beschreiben / Analysieren / HTML erzeugen === @router.post("/process-all") def process_all_scans(): """ Einfache Dummy-Verarbeitung aller Eingangsdateien. Dient vor allem als Fallback / Debug. """ processed = [] skipped = [] for f in EINGANG_DIR.iterdir(): if is_valid_input_file(f): out = dummy_process_scan(f) processed.append(out.name) else: skipped.append(f.name) return { "status": "OK", "processed_files": processed, "skipped": skipped, } @router.post("/remove-handwriting-all") def remove_handwriting_all(): """ MVP-Version: erzeugt *_clean-Bilder im Ordner Bereinigt. Die eigentliche inhaltliche „Bereinigung“ (keine Handschrift, keine durchgestrichenen Wörter) passiert später beim HTML-Neuaufbau. """ cleaned = [] errors = [] skipped = [] for f in EINGANG_DIR.iterdir(): if not is_valid_input_file(f): skipped.append(f.name) continue if f.suffix.lower() not in {".jpg", ".jpeg", ".png"}: skipped.append(f.name) continue try: out = remove_handwriting_from_scan(f) cleaned.append(out.name) except Exception as e: errors.append({"file": f.name, "error": str(e)}) return { "status": "OK" if cleaned else "ERROR", "cleaned": cleaned, "errors": errors, "skipped": skipped, } @router.post("/describe-all") def describe_all_scans(): """ Vision-Modell beschreibt alle gültigen Eingangsdateien kurz. Ergebnis: *_beschreibung.txt in Bereinigt. """ described = [] errors = [] skipped = [] for f in EINGANG_DIR.iterdir(): if not is_valid_input_file(f): skipped.append(f.name) continue try: out = describe_scan_with_ai(f) described.append(out.name) except Exception as e: errors.append({"file": f.name, "error": str(e)}) return {"status": "OK", "described": described, "errors": errors, "skipped": skipped} @router.post("/analyze-all") def analyze_all_scans(): """ Vision-Modell analysiert Struktur aller Eingangsdateien. Ergebnis: *_analyse.json in Bereinigt. """ analyzed = [] errors = [] skipped = [] for f in EINGANG_DIR.iterdir(): if not is_valid_input_file(f): skipped.append(f.name) continue try: out = analyze_scan_structure_with_ai(f) analyzed.append(out.name) except Exception as e: errors.append({"file": f.name, "error": str(e)}) return {"status": "OK", "analyzed": analyzed, "errors": errors, "skipped": skipped} @router.post("/generate-clean") def generate_clean_worksheets(): """ Baut aus allen *_analyse.json Dateien saubere HTML-Arbeitsblätter. Ergebnis: *_clean.html in Bereinigt. """ generated = [] errors = [] for f in BEREINIGT_DIR.iterdir(): if f.suffix == ".json" and f.name.endswith("_analyse.json"): try: out = build_clean_html_from_analysis(f) generated.append(out.name) except Exception as e: errors.append({"file": f.name, "error": str(e)}) return {"status": "OK", "generated": generated, "errors": errors} # === Mapping Alt vs. Neu + HTML-/Image-Auslieferung === @router.get("/worksheet-pairs") def list_worksheet_pairs(): """ Für jede Datei im Eingang wird nach einem passenden „sauberen“ Pendant gesucht: - HTML: - _clean.html - __clean.html (kompatibel zu früherem Bug mit doppeltem Unterstrich) - .html - Bild: - _clean.(jpg|jpeg|png|JPG|JPEG|PNG) - __clean.(...) (falls so erzeugt) """ pairs = [] for f in EINGANG_DIR.iterdir(): if not is_valid_input_file(f): continue base = f.stem clean_html = None clean_image = None # HTML-Kandidaten html_candidates = [ f"{base}_clean.html", f"{base}__clean.html", f"{base}.html", ] for cand in html_candidates: p = BEREINIGT_DIR / cand if p.exists() and p.is_file(): clean_html = cand break # Bild-Kandidaten (immer suchen, unabhängig von HTML) exts = [".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG"] found = False for ext in exts: for suffix in ["_clean", "__clean"]: cand_img = f"{base}{suffix}{ext}" p = BEREINIGT_DIR / cand_img if p.exists() and p.is_file(): clean_image = cand_img found = True break if found: break pairs.append( { "original": f.name, "clean_html": clean_html, "clean_image": clean_image, } ) return {"pairs": pairs} @router.get("/clean-html/{filename}", response_class=HTMLResponse) def get_clean_html(filename: str): """ Liefert eine der erzeugten clean-HTML-Dateien aus dem Ordner Bereinigt. Wird im Frontend für die „neu aufgebautes Arbeitsblatt"-Vorschau genutzt. """ path = BEREINIGT_DIR / filename if not path.exists() or not path.is_file() or path.suffix != ".html": return HTMLResponse( "

Kein neu aufgebautes Arbeitsblatt gefunden.

", status_code=404, ) content = path.read_text(encoding="utf-8") return HTMLResponse(content) # === MULTIPLE CHOICE === @router.post("/generate-mc") def generate_mc_all(): """ Generiert Multiple-Choice-Fragen für alle analysierten Arbeitsblätter. Voraussetzung: Die Analyse (*_analyse.json) muss bereits existieren. Ergebnis: *_mc.json Dateien im Ordner Bereinigt. Die MC-Fragen: - Entsprechen dem Schwierigkeitsgrad des Originals - Haben zufällig angeordnete Antworten (nicht immer A = richtig) """ generated = [] errors = [] skipped = [] for f in BEREINIGT_DIR.iterdir(): if f.suffix == ".json" and f.name.endswith("_analyse.json"): try: out = generate_mc_from_analysis(f) generated.append(out.name) except Exception as e: errors.append({"file": f.name, "error": str(e)}) elif f.suffix == ".json": skipped.append(f.name) return { "status": "OK" if generated else "ERROR", "generated": generated, "errors": errors, "skipped": skipped, } @router.get("/mc-data/{filename}") def get_mc_data(filename: str): """ Liefert die generierten MC-Fragen für ein Arbeitsblatt. Args: filename: Name der Original-Datei (z.B. "2024-12-10_mathe.jpg") oder der MC-Datei (z.B. "2024-12-10_mathe_mc.json") Returns: JSON mit questions und metadata """ import json # Versuche verschiedene Dateinamen-Formate base = filename if base.endswith("_mc.json"): mc_filename = base else: # Entferne Dateiendung und füge _mc.json hinzu base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename mc_filename = f"{base_stem}_mc.json" path = BEREINIGT_DIR / mc_filename if not path.exists() or not path.is_file(): return {"status": "NOT_FOUND", "message": f"MC-Daten nicht gefunden: {mc_filename}"} try: data = json.loads(path.read_text(encoding="utf-8")) return {"status": "OK", "data": data, "filename": mc_filename} except json.JSONDecodeError as e: return {"status": "ERROR", "message": f"Ungültige JSON-Datei: {e}"} @router.get("/mc-list") def list_mc_files(): """ Listet alle generierten MC-Dateien auf. Returns: Liste der MC-Dateien mit zugehörigen Original-Dateinamen """ mc_files = [] for f in BEREINIGT_DIR.iterdir(): if f.suffix == ".json" and f.name.endswith("_mc.json"): # Versuche Original-Datei zu finden base_stem = f.stem.replace("_mc", "") original = None # Suche nach passender Datei im Eingang for ext in [".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG", ".pdf", ".PDF"]: candidate = EINGANG_DIR / f"{base_stem}{ext}" if candidate.exists(): original = candidate.name break mc_files.append({ "mc_file": f.name, "original": original, "base_name": base_stem, }) return {"mc_files": mc_files} # === LÜCKENTEXT === @router.post("/generate-cloze") def generate_cloze_all(target_language: str = "tr"): """ Generiert Lückentexte für alle analysierten Arbeitsblätter. Voraussetzung: Die Analyse (*_analyse.json) muss bereits existieren. Ergebnis: *_cloze.json Dateien im Ordner Bereinigt. Die Lückentexte: - Haben mehrere sinnvolle Lücken pro Satz - Entsprechen dem Schwierigkeitsgrad des Originals - Enthalten eine Übersetzung mit denselben Lücken Args: target_language: Sprachcode für Übersetzung (default: "tr" für Türkisch) Unterstützt: tr, ar, ru, en, fr, es, pl, uk """ generated = [] errors = [] skipped = [] for f in BEREINIGT_DIR.iterdir(): if f.suffix == ".json" and f.name.endswith("_analyse.json"): try: out = generate_cloze_from_analysis(f, target_language) generated.append(out.name) except Exception as e: errors.append({"file": f.name, "error": str(e)}) elif f.suffix == ".json": skipped.append(f.name) return { "status": "OK" if generated else "ERROR", "generated": generated, "errors": errors, "skipped": skipped, "target_language": target_language, } @router.get("/cloze-data/{filename}") def get_cloze_data(filename: str): """ Liefert die generierten Lückentexte für ein Arbeitsblatt. Args: filename: Name der Original-Datei (z.B. "2024-12-10_deutsch.jpg") oder der Cloze-Datei (z.B. "2024-12-10_deutsch_cloze.json") Returns: JSON mit cloze_items und metadata """ import json # Versuche verschiedene Dateinamen-Formate base = filename if base.endswith("_cloze.json"): cloze_filename = base else: # Entferne Dateiendung und füge _cloze.json hinzu base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename cloze_filename = f"{base_stem}_cloze.json" path = BEREINIGT_DIR / cloze_filename if not path.exists() or not path.is_file(): return {"status": "NOT_FOUND", "message": f"Lückentext-Daten nicht gefunden: {cloze_filename}"} try: data = json.loads(path.read_text(encoding="utf-8")) return {"status": "OK", "data": data, "filename": cloze_filename} except json.JSONDecodeError as e: return {"status": "ERROR", "message": f"Ungültige JSON-Datei: {e}"} @router.get("/cloze-list") def list_cloze_files(): """ Listet alle generierten Lückentext-Dateien auf. Returns: Liste der Cloze-Dateien mit zugehörigen Original-Dateinamen """ cloze_files = [] for f in BEREINIGT_DIR.iterdir(): if f.suffix == ".json" and f.name.endswith("_cloze.json"): # Versuche Original-Datei zu finden base_stem = f.stem.replace("_cloze", "") original = None # Suche nach passender Datei im Eingang for ext in [".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG", ".pdf", ".PDF"]: candidate = EINGANG_DIR / f"{base_stem}{ext}" if candidate.exists(): original = candidate.name break cloze_files.append({ "cloze_file": f.name, "original": original, "base_name": base_stem, }) return {"cloze_files": cloze_files} # === FRAGE-ANTWORT (Q&A) MIT LEITNER-SYSTEM === @router.post("/generate-qa") def generate_qa_all(num_questions: int = 8): """ Generiert Frage-Antwort-Paare für alle analysierten Arbeitsblätter. Voraussetzung: Die Analyse (*_analyse.json) muss bereits existieren. Ergebnis: *_qa.json Dateien im Ordner Bereinigt. Die Q&A-Paare: - Fragen sind fast wörtlich aus dem Text entnommen - Schlüsselbegriffe sind markiert für Spaced Repetition - Enthalten Leitner-Box Metadaten für Lernfortschritt Args: num_questions: Anzahl der zu generierenden Fragen (default: 8) """ generated = [] errors = [] skipped = [] for f in BEREINIGT_DIR.iterdir(): if f.suffix == ".json" and f.name.endswith("_analyse.json"): try: out = generate_qa_from_analysis(f, num_questions) generated.append(out.name) except Exception as e: errors.append({"file": f.name, "error": str(e)}) elif f.suffix == ".json": skipped.append(f.name) return { "status": "OK" if generated else "ERROR", "generated": generated, "errors": errors, "skipped": skipped, "num_questions": num_questions, } @router.get("/qa-data/{filename}") def get_qa_data(filename: str): """ Liefert die generierten Q&A-Paare für ein Arbeitsblatt. Args: filename: Name der Original-Datei (z.B. "2024-12-10_deutsch.jpg") oder der QA-Datei (z.B. "2024-12-10_deutsch_qa.json") Returns: JSON mit qa_items und metadata inkl. Leitner-Fortschritt """ import json # Versuche verschiedene Dateinamen-Formate base = filename if base.endswith("_qa.json"): qa_filename = base else: # Entferne Dateiendung und füge _qa.json hinzu base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename qa_filename = f"{base_stem}_qa.json" path = BEREINIGT_DIR / qa_filename if not path.exists() or not path.is_file(): return {"status": "NOT_FOUND", "message": f"Q&A-Daten nicht gefunden: {qa_filename}"} try: data = json.loads(path.read_text(encoding="utf-8")) return {"status": "OK", "data": data, "filename": qa_filename} except json.JSONDecodeError as e: return {"status": "ERROR", "message": f"Ungültige JSON-Datei: {e}"} @router.get("/qa-list") def list_qa_files(): """ Listet alle generierten Q&A-Dateien auf. Returns: Liste der QA-Dateien mit zugehörigen Original-Dateinamen """ qa_files = [] for f in BEREINIGT_DIR.iterdir(): if f.suffix == ".json" and f.name.endswith("_qa.json"): # Versuche Original-Datei zu finden base_stem = f.stem.replace("_qa", "") original = None # Suche nach passender Datei im Eingang for ext in [".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG", ".pdf", ".PDF"]: candidate = EINGANG_DIR / f"{base_stem}{ext}" if candidate.exists(): original = candidate.name break qa_files.append({ "qa_file": f.name, "original": original, "base_name": base_stem, }) return {"qa_files": qa_files} @router.post("/qa-progress") def update_qa_progress(filename: str, item_id: str, correct: bool): """ Aktualisiert den Leitner-Lernfortschritt für eine Q&A-Frage. Das Leitner-System: - Box 0 (neu): Wiederholung nach 1 Tag - Box 1 (gelernt): Wiederholung nach 3 Tagen - Box 2 (gefestigt): Wiederholung nach 7 Tagen - Falsche Antwort: Eine Box zurück, Wiederholung nach 4 Stunden Args: filename: Name der QA-Datei item_id: ID der Frage (z.B. "q_0") correct: True wenn richtig beantwortet, False wenn falsch Returns: Aktualisierter Fortschritt für die Frage """ # Finde QA-Datei base = filename if base.endswith("_qa.json"): qa_filename = base else: base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename qa_filename = f"{base_stem}_qa.json" path = BEREINIGT_DIR / qa_filename if not path.exists() or not path.is_file(): return {"status": "NOT_FOUND", "message": f"Q&A-Daten nicht gefunden: {qa_filename}"} try: result = update_leitner_progress(path, item_id, correct) return {"status": "OK", "progress": result, "filename": qa_filename} except Exception as e: return {"status": "ERROR", "message": str(e)} @router.get("/qa-review/{filename}") def get_qa_review_items(filename: str, limit: int = 5): """ Liefert die nächsten zu wiederholenden Fragen basierend auf dem Leitner-System. Priorisierung: 1. Überfällige Fragen (nach next_review Zeit) 2. Niedrigere Boxen zuerst (Box 0 vor Box 1 vor Box 2) 3. Längere Zeit nicht gesehen Args: filename: Name der QA-Datei limit: Maximale Anzahl zurückzugebender Fragen (default: 5) Returns: Liste der zu wiederholenden Fragen mit Leitner-Metadaten """ # Finde QA-Datei base = filename if base.endswith("_qa.json"): qa_filename = base else: base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename qa_filename = f"{base_stem}_qa.json" path = BEREINIGT_DIR / qa_filename if not path.exists() or not path.is_file(): return {"status": "NOT_FOUND", "message": f"Q&A-Daten nicht gefunden: {qa_filename}"} try: items = get_next_review_items(path, limit) return {"status": "OK", "review_items": items, "filename": qa_filename, "count": len(items)} except Exception as e: return {"status": "ERROR", "message": str(e)} # === PRINT-VERSIONEN === @router.get("/print-qa/{filename}", response_class=HTMLResponse) def get_print_qa(filename: str, show_answers: bool = False): """ Liefert eine druckbare HTML-Version der Q&A-Fragen. Args: filename: Name der QA-Datei oder Original-Datei show_answers: True für Lösungsblatt, False für Fragenblatt Returns: Druckbare HTML-Seite """ # Finde QA-Datei base = filename if base.endswith("_qa.json"): qa_filename = base else: base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename qa_filename = f"{base_stem}_qa.json" path = BEREINIGT_DIR / qa_filename if not path.exists() or not path.is_file(): return HTMLResponse( "

Q&A-Daten nicht gefunden.

", status_code=404, ) try: html = generate_print_version_qa(path, show_answers) return HTMLResponse(html) except Exception as e: return HTMLResponse( f"

Fehler: {e}

", status_code=500, ) @router.get("/print-cloze/{filename}", response_class=HTMLResponse) def get_print_cloze(filename: str, show_answers: bool = False): """ Liefert eine druckbare HTML-Version der Lückentexte. Args: filename: Name der Cloze-Datei oder Original-Datei show_answers: True für Lösungsblatt mit ausgefüllten Lücken, False für Übungsblatt mit Wortbank Returns: Druckbare HTML-Seite """ # Finde Cloze-Datei base = filename if base.endswith("_cloze.json"): cloze_filename = base else: base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename cloze_filename = f"{base_stem}_cloze.json" path = BEREINIGT_DIR / cloze_filename if not path.exists() or not path.is_file(): return HTMLResponse( "

Lückentext-Daten nicht gefunden.

", status_code=404, ) try: html = generate_print_version_cloze(path, show_answers) return HTMLResponse(html) except Exception as e: return HTMLResponse( f"

Fehler: {e}

", status_code=500, ) @router.get("/print-mc/{filename}", response_class=HTMLResponse) def get_print_mc(filename: str, show_answers: bool = False): """ Liefert eine druckbare HTML-Version der Multiple-Choice-Fragen. Args: filename: Name der MC-Datei oder Original-Datei show_answers: True für Lösungsblatt mit markierten Antworten, False für Testblatt zum Ausfüllen Returns: Druckbare HTML-Seite """ # Finde MC-Datei base = filename if base.endswith("_mc.json"): mc_filename = base else: base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename mc_filename = f"{base_stem}_mc.json" path = BEREINIGT_DIR / mc_filename if not path.exists() or not path.is_file(): return HTMLResponse( "

MC-Daten nicht gefunden.

", status_code=404, ) try: html = generate_print_version_mc(path, show_answers) return HTMLResponse(html) except Exception as e: return HTMLResponse( f"

Fehler beim Erstellen der MC-Druckversion: {e}

", status_code=500, ) @router.get("/print-worksheet/{filename}", response_class=HTMLResponse) def get_print_worksheet(filename: str): """ Liefert eine druckoptimierte HTML-Version des neu aufgebauten Arbeitsblatts. - Große, klare Schrift - Schwarz-weiß / Graustufen-tauglich - Direkt druckbar für Eltern """ # Analysedatei finden base = filename.rsplit(".", 1)[0] # Entfernt Dateiendung analysis_path = BEREINIGT_DIR / f"{base}_analyse.json" if not analysis_path.exists(): return HTMLResponse( f"

Keine Analyse gefunden für: {filename}

", status_code=404, ) try: html = generate_print_version_worksheet(analysis_path) return HTMLResponse(html) except Exception as e: return HTMLResponse( f"

Fehler beim Erstellen der Druckversion: {e}

", status_code=500, ) # === MINDMAP LERNPOSTER === @router.post("/generate-mindmap/{filename}") def generate_mindmap(filename: str): """ Generiert eine kindgerechte Mindmap aus einem analysierten Arbeitsblatt. Die Mindmap: - Zeigt das Hauptthema in der Mitte - Gruppiert Fachbegriffe in farbige Kategorien - Enthält Emojis für bessere visuelle Orientierung - Kann als A3 Poster gedruckt werden Args: filename: Name der Original-Datei (z.B. "2024-12-10_biologie.jpg") Returns: Status und Dateiname der generierten Mindmap """ # Analysedatei finden base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename analysis_path = BEREINIGT_DIR / f"{base_stem}_analyse.json" if not analysis_path.exists(): return { "status": "NOT_FOUND", "message": f"Keine Analyse gefunden für: {filename}. Bitte zuerst analysieren." } try: # Mindmap-Daten generieren mindmap_data = generate_mindmap_data(analysis_path) # Mindmap speichern mindmap_path = save_mindmap_for_worksheet(analysis_path, mindmap_data) return { "status": "OK", "message": "Mindmap erfolgreich generiert", "filename": mindmap_path.name, "topic": mindmap_data.get("topic", ""), "categories_count": len(mindmap_data.get("categories", [])), } except Exception as e: return { "status": "ERROR", "message": f"Fehler bei der Mindmap-Generierung: {e}" } @router.get("/mindmap-data/{filename}") def get_mindmap_data(filename: str): """ Liefert die generierten Mindmap-Daten für ein Arbeitsblatt. Args: filename: Name der Original-Datei oder der Mindmap-Datei Returns: JSON mit topic, subject und categories """ import json # Versuche verschiedene Dateinamen-Formate base = filename if base.endswith("_mindmap.json"): mindmap_filename = base else: base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename mindmap_filename = f"{base_stem}_mindmap.json" path = BEREINIGT_DIR / mindmap_filename if not path.exists() or not path.is_file(): return {"status": "NOT_FOUND", "message": f"Mindmap-Daten nicht gefunden: {mindmap_filename}"} try: data = json.loads(path.read_text(encoding="utf-8")) return {"status": "OK", "data": data, "filename": mindmap_filename} except json.JSONDecodeError as e: return {"status": "ERROR", "message": f"Ungültige JSON-Datei: {e}"} @router.get("/mindmap-html/{filename}", response_class=HTMLResponse) def get_mindmap_html(filename: str, format: str = "a3"): """ Liefert die Mindmap als druckbares HTML mit SVG. Args: filename: Name der Original-Datei oder der Mindmap-Datei format: Druckformat - "a3" (Standard, Poster) oder "a4" Returns: HTML-Seite mit SVG-Mindmap, druckoptimiert """ import json # Versuche verschiedene Dateinamen-Formate base = filename if base.endswith("_mindmap.json"): mindmap_filename = base else: base_stem = filename.rsplit(".", 1)[0] if "." in filename else filename mindmap_filename = f"{base_stem}_mindmap.json" path = BEREINIGT_DIR / mindmap_filename if not path.exists() or not path.is_file(): return HTMLResponse( "

Mindmap-Daten nicht gefunden. Bitte zuerst generieren.

", status_code=404, ) try: data = json.loads(path.read_text(encoding="utf-8")) html = generate_mindmap_html(data, format) return HTMLResponse(html) except json.JSONDecodeError: return HTMLResponse( "

Ungültige Mindmap-Datei.

", status_code=500, ) except Exception as e: return HTMLResponse( f"

Fehler beim Erstellen der Mindmap: {e}

", status_code=500, ) @router.get("/mindmap-list") def list_mindmap_files(): """ Listet alle generierten Mindmap-Dateien auf. Returns: Liste der Mindmap-Dateien mit zugehörigen Original-Dateinamen """ mindmap_files = [] for f in BEREINIGT_DIR.iterdir(): if f.suffix == ".json" and f.name.endswith("_mindmap.json"): # Versuche Original-Datei zu finden base_stem = f.stem.replace("_mindmap", "") original = None # Suche nach passender Datei im Eingang for ext in [".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG", ".pdf", ".PDF"]: candidate = EINGANG_DIR / f"{base_stem}{ext}" if candidate.exists(): original = candidate.name break mindmap_files.append({ "mindmap_file": f.name, "original": original, "base_name": base_stem, }) return {"mindmap_files": mindmap_files}