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/original_service.py
Benjamin Admin bfdaf63ba9 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

1042 lines
31 KiB
Python

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 """
<html>
<body>
<h1>Arbeitsblätter hochladen</h1>
<form action="/upload-multi" method="post" enctype="multipart/form-data">
<input type="file" name="files" multiple required>
<button type="submit">Hochladen</button>
</form>
</body>
</html>
"""
@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:
- <stem>_clean.html
- <stem>__clean.html (kompatibel zu früherem Bug mit doppeltem Unterstrich)
- <stem>.html
- Bild:
- <stem>_clean.(jpg|jpeg|png|JPG|JPEG|PNG)
- <stem>__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(
"<html><body><p>Kein neu aufgebautes Arbeitsblatt gefunden.</p></body></html>",
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(
"<html><body><p>Q&A-Daten nicht gefunden.</p></body></html>",
status_code=404,
)
try:
html = generate_print_version_qa(path, show_answers)
return HTMLResponse(html)
except Exception as e:
return HTMLResponse(
f"<html><body><p>Fehler: {e}</p></body></html>",
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(
"<html><body><p>Lückentext-Daten nicht gefunden.</p></body></html>",
status_code=404,
)
try:
html = generate_print_version_cloze(path, show_answers)
return HTMLResponse(html)
except Exception as e:
return HTMLResponse(
f"<html><body><p>Fehler: {e}</p></body></html>",
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(
"<html><body><p>MC-Daten nicht gefunden.</p></body></html>",
status_code=404,
)
try:
html = generate_print_version_mc(path, show_answers)
return HTMLResponse(html)
except Exception as e:
return HTMLResponse(
f"<html><body><p>Fehler beim Erstellen der MC-Druckversion: {e}</p></body></html>",
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"<html><body><p>Keine Analyse gefunden für: {filename}</p></body></html>",
status_code=404,
)
try:
html = generate_print_version_worksheet(analysis_path)
return HTMLResponse(html)
except Exception as e:
return HTMLResponse(
f"<html><body><p>Fehler beim Erstellen der Druckversion: {e}</p></body></html>",
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(
"<html><body><p>Mindmap-Daten nicht gefunden. Bitte zuerst generieren.</p></body></html>",
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(
"<html><body><p>Ungültige Mindmap-Datei.</p></body></html>",
status_code=500,
)
except Exception as e:
return HTMLResponse(
f"<html><body><p>Fehler beim Erstellen der Mindmap: {e}</p></body></html>",
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}