Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
684 lines
22 KiB
Python
684 lines
22 KiB
Python
"""
|
|
Correction API - REST API für Klassenarbeits-Korrektur.
|
|
|
|
Workflow:
|
|
1. Upload: Gescannte Klassenarbeit hochladen
|
|
2. OCR: Text aus Handschrift extrahieren
|
|
3. Analyse: Antworten analysieren und bewerten
|
|
4. Feedback: KI-generiertes Feedback erstellen
|
|
5. Export: Korrigierte Arbeit als PDF exportieren
|
|
|
|
Integriert:
|
|
- FileProcessor für OCR
|
|
- PDFService für Export
|
|
- LLM für Analyse und Feedback
|
|
"""
|
|
|
|
import logging
|
|
import uuid
|
|
import os
|
|
from datetime import datetime
|
|
from typing import List, Dict, Any, Optional
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException, UploadFile, File, Form, BackgroundTasks
|
|
from pydantic import BaseModel, Field
|
|
|
|
# FileProcessor requires OpenCV with libGL - make optional for CI
|
|
try:
|
|
from services.file_processor import FileProcessor, ProcessingResult
|
|
_ocr_available = True
|
|
except (ImportError, OSError):
|
|
FileProcessor = None # type: ignore
|
|
ProcessingResult = None # type: ignore
|
|
_ocr_available = False
|
|
|
|
# PDF service requires WeasyPrint with system libraries - make optional for CI
|
|
try:
|
|
from services.pdf_service import PDFService, CorrectionData, StudentInfo
|
|
_pdf_available = True
|
|
except (ImportError, OSError):
|
|
PDFService = None # type: ignore
|
|
CorrectionData = None # type: ignore
|
|
StudentInfo = None # type: ignore
|
|
_pdf_available = False
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(
|
|
prefix="/corrections",
|
|
tags=["corrections"],
|
|
)
|
|
|
|
# Upload directory
|
|
UPLOAD_DIR = Path("/tmp/corrections")
|
|
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
# ============================================================================
|
|
# Enums and Models
|
|
# ============================================================================
|
|
|
|
class CorrectionStatus(str, Enum):
|
|
"""Status einer Korrektur."""
|
|
UPLOADED = "uploaded" # Datei hochgeladen
|
|
PROCESSING = "processing" # OCR läuft
|
|
OCR_COMPLETE = "ocr_complete" # OCR abgeschlossen
|
|
ANALYZING = "analyzing" # Analyse läuft
|
|
ANALYZED = "analyzed" # Analyse abgeschlossen
|
|
REVIEWING = "reviewing" # Lehrkraft prüft
|
|
COMPLETED = "completed" # Korrektur abgeschlossen
|
|
ERROR = "error" # Fehler aufgetreten
|
|
|
|
|
|
class AnswerEvaluation(BaseModel):
|
|
"""Bewertung einer einzelnen Antwort."""
|
|
question_number: int
|
|
extracted_text: str
|
|
points_possible: float
|
|
points_awarded: float
|
|
feedback: str
|
|
is_correct: bool
|
|
confidence: float # 0-1, wie sicher die OCR/Analyse ist
|
|
|
|
|
|
class CorrectionCreate(BaseModel):
|
|
"""Request zum Erstellen einer neuen Korrektur."""
|
|
student_id: str
|
|
student_name: str
|
|
class_name: str
|
|
exam_title: str
|
|
subject: str
|
|
max_points: float = Field(default=100.0, ge=0)
|
|
expected_answers: Optional[Dict[str, str]] = None # Musterlösung
|
|
|
|
|
|
class CorrectionUpdate(BaseModel):
|
|
"""Request zum Aktualisieren einer Korrektur."""
|
|
evaluations: Optional[List[AnswerEvaluation]] = None
|
|
total_points: Optional[float] = None
|
|
grade: Optional[str] = None
|
|
teacher_notes: Optional[str] = None
|
|
status: Optional[CorrectionStatus] = None
|
|
|
|
|
|
class Correction(BaseModel):
|
|
"""Eine Korrektur."""
|
|
id: str
|
|
student_id: str
|
|
student_name: str
|
|
class_name: str
|
|
exam_title: str
|
|
subject: str
|
|
max_points: float
|
|
total_points: float = 0.0
|
|
percentage: float = 0.0
|
|
grade: Optional[str] = None
|
|
status: CorrectionStatus
|
|
file_path: Optional[str] = None
|
|
extracted_text: Optional[str] = None
|
|
evaluations: List[AnswerEvaluation] = []
|
|
teacher_notes: Optional[str] = None
|
|
ai_feedback: Optional[str] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class CorrectionResponse(BaseModel):
|
|
"""Response für eine Korrektur."""
|
|
success: bool
|
|
correction: Optional[Correction] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
class OCRResponse(BaseModel):
|
|
"""Response für OCR-Ergebnis."""
|
|
success: bool
|
|
extracted_text: Optional[str] = None
|
|
regions: List[Dict[str, Any]] = []
|
|
confidence: float = 0.0
|
|
error: Optional[str] = None
|
|
|
|
|
|
class AnalysisResponse(BaseModel):
|
|
"""Response für Analyse-Ergebnis."""
|
|
success: bool
|
|
evaluations: List[AnswerEvaluation] = []
|
|
total_points: float = 0.0
|
|
percentage: float = 0.0
|
|
suggested_grade: Optional[str] = None
|
|
ai_feedback: Optional[str] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
# ============================================================================
|
|
# In-Memory Storage (später durch DB ersetzen)
|
|
# ============================================================================
|
|
|
|
_corrections: Dict[str, Correction] = {}
|
|
|
|
|
|
# ============================================================================
|
|
# Helper Functions
|
|
# ============================================================================
|
|
|
|
def _calculate_grade(percentage: float) -> str:
|
|
"""Berechnet Note aus Prozent (deutsches System)."""
|
|
if percentage >= 92:
|
|
return "1"
|
|
elif percentage >= 81:
|
|
return "2"
|
|
elif percentage >= 67:
|
|
return "3"
|
|
elif percentage >= 50:
|
|
return "4"
|
|
elif percentage >= 30:
|
|
return "5"
|
|
else:
|
|
return "6"
|
|
|
|
|
|
def _generate_ai_feedback(
|
|
evaluations: List[AnswerEvaluation],
|
|
total_points: float,
|
|
max_points: float,
|
|
subject: str
|
|
) -> str:
|
|
"""Generiert KI-Feedback basierend auf Bewertung."""
|
|
# Ohne LLM: Einfaches Template-basiertes Feedback
|
|
percentage = (total_points / max_points * 100) if max_points > 0 else 0
|
|
correct_count = sum(1 for e in evaluations if e.is_correct)
|
|
total_count = len(evaluations)
|
|
|
|
if percentage >= 90:
|
|
intro = "Hervorragende Leistung!"
|
|
elif percentage >= 75:
|
|
intro = "Gute Arbeit!"
|
|
elif percentage >= 60:
|
|
intro = "Insgesamt eine solide Leistung."
|
|
elif percentage >= 50:
|
|
intro = "Die Arbeit zeigt Grundkenntnisse, aber es gibt Verbesserungsbedarf."
|
|
else:
|
|
intro = "Es sind deutliche Wissenslücken erkennbar."
|
|
|
|
# Finde Verbesserungsbereiche
|
|
weak_areas = [e for e in evaluations if not e.is_correct]
|
|
strengths = [e for e in evaluations if e.is_correct and e.confidence > 0.8]
|
|
|
|
feedback_parts = [intro]
|
|
|
|
if strengths:
|
|
feedback_parts.append(
|
|
f"Besonders gut gelöst: Aufgabe(n) {', '.join(str(s.question_number) for s in strengths[:3])}."
|
|
)
|
|
|
|
if weak_areas:
|
|
feedback_parts.append(
|
|
f"Übungsbedarf bei: Aufgabe(n) {', '.join(str(w.question_number) for w in weak_areas[:3])}."
|
|
)
|
|
|
|
feedback_parts.append(
|
|
f"Ergebnis: {correct_count} von {total_count} Aufgaben korrekt ({percentage:.1f}%)."
|
|
)
|
|
|
|
return " ".join(feedback_parts)
|
|
|
|
|
|
async def _process_ocr(correction_id: str, file_path: str):
|
|
"""Background Task für OCR-Verarbeitung."""
|
|
correction = _corrections.get(correction_id)
|
|
if not correction:
|
|
return
|
|
|
|
try:
|
|
correction.status = CorrectionStatus.PROCESSING
|
|
_corrections[correction_id] = correction
|
|
|
|
# OCR durchführen
|
|
processor = FileProcessor()
|
|
result = processor.process_file(file_path)
|
|
|
|
if result.success and result.text:
|
|
correction.extracted_text = result.text
|
|
correction.status = CorrectionStatus.OCR_COMPLETE
|
|
else:
|
|
correction.status = CorrectionStatus.ERROR
|
|
|
|
correction.updated_at = datetime.utcnow()
|
|
_corrections[correction_id] = correction
|
|
|
|
except Exception as e:
|
|
logger.error(f"OCR error for {correction_id}: {e}")
|
|
correction.status = CorrectionStatus.ERROR
|
|
correction.updated_at = datetime.utcnow()
|
|
_corrections[correction_id] = correction
|
|
|
|
|
|
# ============================================================================
|
|
# API Endpoints
|
|
# ============================================================================
|
|
|
|
@router.post("/", response_model=CorrectionResponse)
|
|
async def create_correction(data: CorrectionCreate):
|
|
"""
|
|
Erstellt eine neue Korrektur.
|
|
|
|
Noch ohne Datei - diese wird separat hochgeladen.
|
|
"""
|
|
correction_id = str(uuid.uuid4())
|
|
now = datetime.utcnow()
|
|
|
|
correction = Correction(
|
|
id=correction_id,
|
|
student_id=data.student_id,
|
|
student_name=data.student_name,
|
|
class_name=data.class_name,
|
|
exam_title=data.exam_title,
|
|
subject=data.subject,
|
|
max_points=data.max_points,
|
|
status=CorrectionStatus.UPLOADED,
|
|
created_at=now,
|
|
updated_at=now
|
|
)
|
|
|
|
_corrections[correction_id] = correction
|
|
logger.info(f"Created correction {correction_id} for {data.student_name}")
|
|
|
|
return CorrectionResponse(success=True, correction=correction)
|
|
|
|
|
|
@router.post("/{correction_id}/upload", response_model=CorrectionResponse)
|
|
async def upload_exam(
|
|
correction_id: str,
|
|
background_tasks: BackgroundTasks,
|
|
file: UploadFile = File(...)
|
|
):
|
|
"""
|
|
Lädt gescannte Klassenarbeit hoch und startet OCR.
|
|
|
|
Unterstützte Formate: PDF, PNG, JPG, JPEG
|
|
"""
|
|
correction = _corrections.get(correction_id)
|
|
if not correction:
|
|
raise HTTPException(status_code=404, detail="Korrektur nicht gefunden")
|
|
|
|
# Validiere Dateiformat
|
|
allowed_extensions = {".pdf", ".png", ".jpg", ".jpeg"}
|
|
file_ext = Path(file.filename).suffix.lower() if file.filename else ""
|
|
|
|
if file_ext not in allowed_extensions:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Ungültiges Dateiformat. Erlaubt: {', '.join(allowed_extensions)}"
|
|
)
|
|
|
|
# Speichere Datei
|
|
file_path = UPLOAD_DIR / f"{correction_id}{file_ext}"
|
|
|
|
try:
|
|
content = await file.read()
|
|
with open(file_path, "wb") as f:
|
|
f.write(content)
|
|
|
|
correction.file_path = str(file_path)
|
|
correction.updated_at = datetime.utcnow()
|
|
_corrections[correction_id] = correction
|
|
|
|
# Starte OCR im Hintergrund
|
|
background_tasks.add_task(_process_ocr, correction_id, str(file_path))
|
|
|
|
logger.info(f"Uploaded file for correction {correction_id}: {file.filename}")
|
|
|
|
return CorrectionResponse(success=True, correction=correction)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Upload error: {e}")
|
|
return CorrectionResponse(success=False, error=str(e))
|
|
|
|
|
|
@router.get("/{correction_id}", response_model=CorrectionResponse)
|
|
async def get_correction(correction_id: str):
|
|
"""Ruft eine Korrektur ab."""
|
|
correction = _corrections.get(correction_id)
|
|
if not correction:
|
|
raise HTTPException(status_code=404, detail="Korrektur nicht gefunden")
|
|
|
|
return CorrectionResponse(success=True, correction=correction)
|
|
|
|
|
|
@router.get("/", response_model=Dict[str, Any])
|
|
async def list_corrections(
|
|
class_name: Optional[str] = None,
|
|
status: Optional[CorrectionStatus] = None,
|
|
limit: int = 50
|
|
):
|
|
"""Listet Korrekturen auf, optional gefiltert."""
|
|
corrections = list(_corrections.values())
|
|
|
|
if class_name:
|
|
corrections = [c for c in corrections if c.class_name == class_name]
|
|
|
|
if status:
|
|
corrections = [c for c in corrections if c.status == status]
|
|
|
|
# Sortiere nach Erstellungsdatum (neueste zuerst)
|
|
corrections.sort(key=lambda x: x.created_at, reverse=True)
|
|
|
|
return {
|
|
"total": len(corrections),
|
|
"corrections": [c.dict() for c in corrections[:limit]]
|
|
}
|
|
|
|
|
|
@router.post("/{correction_id}/analyze", response_model=AnalysisResponse)
|
|
async def analyze_correction(
|
|
correction_id: str,
|
|
expected_answers: Optional[Dict[str, str]] = None
|
|
):
|
|
"""
|
|
Analysiert die extrahierten Antworten.
|
|
|
|
Optional mit Musterlösung für automatische Bewertung.
|
|
"""
|
|
correction = _corrections.get(correction_id)
|
|
if not correction:
|
|
raise HTTPException(status_code=404, detail="Korrektur nicht gefunden")
|
|
|
|
if correction.status not in [CorrectionStatus.OCR_COMPLETE, CorrectionStatus.ANALYZED]:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Korrektur im falschen Status: {correction.status}"
|
|
)
|
|
|
|
if not correction.extracted_text:
|
|
raise HTTPException(status_code=400, detail="Kein extrahierter Text vorhanden")
|
|
|
|
try:
|
|
correction.status = CorrectionStatus.ANALYZING
|
|
_corrections[correction_id] = correction
|
|
|
|
# Einfache Analyse ohne LLM
|
|
# Teile Text in Abschnitte (simuliert Aufgabenerkennung)
|
|
text_parts = correction.extracted_text.split('\n\n')
|
|
evaluations = []
|
|
|
|
for i, part in enumerate(text_parts[:10], start=1): # Max 10 Aufgaben
|
|
if len(part.strip()) < 5:
|
|
continue
|
|
|
|
# Simulierte Bewertung
|
|
# In Produktion würde hier LLM-basierte Analyse stattfinden
|
|
expected = expected_answers.get(str(i), "") if expected_answers else ""
|
|
|
|
# Einfacher Textvergleich (in Produktion: semantischer Vergleich)
|
|
is_correct = bool(expected and expected.lower() in part.lower())
|
|
points = correction.max_points / len(text_parts) if text_parts else 0
|
|
|
|
evaluation = AnswerEvaluation(
|
|
question_number=i,
|
|
extracted_text=part[:200], # Kürzen für Response
|
|
points_possible=points,
|
|
points_awarded=points if is_correct else points * 0.5, # Teilpunkte
|
|
feedback=f"Antwort zu Aufgabe {i}" + (" korrekt." if is_correct else " mit Verbesserungsbedarf."),
|
|
is_correct=is_correct,
|
|
confidence=0.7 # Simulierte Confidence
|
|
)
|
|
evaluations.append(evaluation)
|
|
|
|
# Berechne Gesamtergebnis
|
|
total_points = sum(e.points_awarded for e in evaluations)
|
|
percentage = (total_points / correction.max_points * 100) if correction.max_points > 0 else 0
|
|
suggested_grade = _calculate_grade(percentage)
|
|
|
|
# Generiere Feedback
|
|
ai_feedback = _generate_ai_feedback(
|
|
evaluations, total_points, correction.max_points, correction.subject
|
|
)
|
|
|
|
# Aktualisiere Korrektur
|
|
correction.evaluations = evaluations
|
|
correction.total_points = total_points
|
|
correction.percentage = percentage
|
|
correction.grade = suggested_grade
|
|
correction.ai_feedback = ai_feedback
|
|
correction.status = CorrectionStatus.ANALYZED
|
|
correction.updated_at = datetime.utcnow()
|
|
_corrections[correction_id] = correction
|
|
|
|
logger.info(f"Analysis complete for {correction_id}: {total_points}/{correction.max_points}")
|
|
|
|
return AnalysisResponse(
|
|
success=True,
|
|
evaluations=evaluations,
|
|
total_points=total_points,
|
|
percentage=percentage,
|
|
suggested_grade=suggested_grade,
|
|
ai_feedback=ai_feedback
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Analysis error: {e}")
|
|
correction.status = CorrectionStatus.ERROR
|
|
_corrections[correction_id] = correction
|
|
return AnalysisResponse(success=False, error=str(e))
|
|
|
|
|
|
@router.put("/{correction_id}", response_model=CorrectionResponse)
|
|
async def update_correction(correction_id: str, data: CorrectionUpdate):
|
|
"""
|
|
Aktualisiert eine Korrektur.
|
|
|
|
Ermöglicht manuelle Anpassungen durch die Lehrkraft.
|
|
"""
|
|
correction = _corrections.get(correction_id)
|
|
if not correction:
|
|
raise HTTPException(status_code=404, detail="Korrektur nicht gefunden")
|
|
|
|
if data.evaluations is not None:
|
|
correction.evaluations = data.evaluations
|
|
correction.total_points = sum(e.points_awarded for e in data.evaluations)
|
|
correction.percentage = (
|
|
correction.total_points / correction.max_points * 100
|
|
) if correction.max_points > 0 else 0
|
|
|
|
if data.total_points is not None:
|
|
correction.total_points = data.total_points
|
|
correction.percentage = (
|
|
data.total_points / correction.max_points * 100
|
|
) if correction.max_points > 0 else 0
|
|
|
|
if data.grade is not None:
|
|
correction.grade = data.grade
|
|
|
|
if data.teacher_notes is not None:
|
|
correction.teacher_notes = data.teacher_notes
|
|
|
|
if data.status is not None:
|
|
correction.status = data.status
|
|
|
|
correction.updated_at = datetime.utcnow()
|
|
_corrections[correction_id] = correction
|
|
|
|
return CorrectionResponse(success=True, correction=correction)
|
|
|
|
|
|
@router.post("/{correction_id}/complete", response_model=CorrectionResponse)
|
|
async def complete_correction(correction_id: str):
|
|
"""Markiert Korrektur als abgeschlossen."""
|
|
correction = _corrections.get(correction_id)
|
|
if not correction:
|
|
raise HTTPException(status_code=404, detail="Korrektur nicht gefunden")
|
|
|
|
correction.status = CorrectionStatus.COMPLETED
|
|
correction.updated_at = datetime.utcnow()
|
|
_corrections[correction_id] = correction
|
|
|
|
logger.info(f"Correction {correction_id} completed: {correction.grade}")
|
|
|
|
return CorrectionResponse(success=True, correction=correction)
|
|
|
|
|
|
@router.get("/{correction_id}/export-pdf")
|
|
async def export_correction_pdf(correction_id: str):
|
|
"""
|
|
Exportiert korrigierte Arbeit als PDF.
|
|
|
|
Enthält:
|
|
- Originalscan
|
|
- Bewertungen
|
|
- Feedback
|
|
- Gesamtergebnis
|
|
"""
|
|
correction = _corrections.get(correction_id)
|
|
if not correction:
|
|
raise HTTPException(status_code=404, detail="Korrektur nicht gefunden")
|
|
|
|
try:
|
|
pdf_service = PDFService()
|
|
|
|
# Erstelle CorrectionData
|
|
correction_data = CorrectionData(
|
|
student=StudentInfo(
|
|
student_id=correction.student_id,
|
|
name=correction.student_name,
|
|
class_name=correction.class_name
|
|
),
|
|
exam_title=correction.exam_title,
|
|
subject=correction.subject,
|
|
date=correction.created_at.strftime("%d.%m.%Y"),
|
|
max_points=correction.max_points,
|
|
achieved_points=correction.total_points,
|
|
grade=correction.grade or "",
|
|
percentage=correction.percentage,
|
|
corrections=[
|
|
{
|
|
"question": f"Aufgabe {e.question_number}",
|
|
"answer": e.extracted_text,
|
|
"points": f"{e.points_awarded}/{e.points_possible}",
|
|
"feedback": e.feedback
|
|
}
|
|
for e in correction.evaluations
|
|
],
|
|
teacher_notes=correction.teacher_notes or "",
|
|
ai_feedback=correction.ai_feedback or ""
|
|
)
|
|
|
|
# Generiere PDF
|
|
pdf_bytes = pdf_service.generate_correction_pdf(correction_data)
|
|
|
|
from fastapi.responses import Response
|
|
|
|
return Response(
|
|
content=pdf_bytes,
|
|
media_type="application/pdf",
|
|
headers={
|
|
"Content-Disposition": f'attachment; filename="korrektur_{correction.student_name}_{correction.exam_title}.pdf"'
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"PDF export error: {e}")
|
|
raise HTTPException(status_code=500, detail=f"PDF-Export fehlgeschlagen: {str(e)}")
|
|
|
|
|
|
@router.delete("/{correction_id}")
|
|
async def delete_correction(correction_id: str):
|
|
"""Löscht eine Korrektur."""
|
|
if correction_id not in _corrections:
|
|
raise HTTPException(status_code=404, detail="Korrektur nicht gefunden")
|
|
|
|
correction = _corrections[correction_id]
|
|
|
|
# Lösche auch die hochgeladene Datei
|
|
if correction.file_path and os.path.exists(correction.file_path):
|
|
try:
|
|
os.remove(correction.file_path)
|
|
except Exception as e:
|
|
logger.warning(f"Could not delete file {correction.file_path}: {e}")
|
|
|
|
del _corrections[correction_id]
|
|
logger.info(f"Deleted correction {correction_id}")
|
|
|
|
return {"status": "deleted", "id": correction_id}
|
|
|
|
|
|
@router.get("/class/{class_name}/summary")
|
|
async def get_class_summary(class_name: str):
|
|
"""
|
|
Gibt Zusammenfassung für eine Klasse zurück.
|
|
|
|
Enthält Statistiken über alle Korrekturen der Klasse.
|
|
"""
|
|
class_corrections = [
|
|
c for c in _corrections.values()
|
|
if c.class_name == class_name and c.status == CorrectionStatus.COMPLETED
|
|
]
|
|
|
|
if not class_corrections:
|
|
return {
|
|
"class_name": class_name,
|
|
"total_students": 0,
|
|
"average_percentage": 0,
|
|
"grade_distribution": {},
|
|
"corrections": []
|
|
}
|
|
|
|
# Berechne Statistiken
|
|
percentages = [c.percentage for c in class_corrections]
|
|
average_percentage = sum(percentages) / len(percentages) if percentages else 0
|
|
|
|
# Notenverteilung
|
|
grade_distribution = {}
|
|
for c in class_corrections:
|
|
grade = c.grade or "?"
|
|
grade_distribution[grade] = grade_distribution.get(grade, 0) + 1
|
|
|
|
return {
|
|
"class_name": class_name,
|
|
"total_students": len(class_corrections),
|
|
"average_percentage": round(average_percentage, 1),
|
|
"average_points": round(
|
|
sum(c.total_points for c in class_corrections) / len(class_corrections), 1
|
|
),
|
|
"grade_distribution": grade_distribution,
|
|
"corrections": [
|
|
{
|
|
"id": c.id,
|
|
"student_name": c.student_name,
|
|
"total_points": c.total_points,
|
|
"percentage": c.percentage,
|
|
"grade": c.grade
|
|
}
|
|
for c in sorted(class_corrections, key=lambda x: x.student_name)
|
|
]
|
|
}
|
|
|
|
|
|
@router.post("/{correction_id}/ocr/retry", response_model=CorrectionResponse)
|
|
async def retry_ocr(correction_id: str, background_tasks: BackgroundTasks):
|
|
"""
|
|
Wiederholt OCR-Verarbeitung.
|
|
|
|
Nützlich wenn erste Verarbeitung fehlgeschlagen ist.
|
|
"""
|
|
correction = _corrections.get(correction_id)
|
|
if not correction:
|
|
raise HTTPException(status_code=404, detail="Korrektur nicht gefunden")
|
|
|
|
if not correction.file_path:
|
|
raise HTTPException(status_code=400, detail="Keine Datei vorhanden")
|
|
|
|
if not os.path.exists(correction.file_path):
|
|
raise HTTPException(status_code=400, detail="Datei nicht mehr vorhanden")
|
|
|
|
# Starte OCR erneut
|
|
correction.status = CorrectionStatus.UPLOADED
|
|
correction.extracted_text = None
|
|
correction.updated_at = datetime.utcnow()
|
|
_corrections[correction_id] = correction
|
|
|
|
background_tasks.add_task(_process_ocr, correction_id, correction.file_path)
|
|
|
|
return CorrectionResponse(success=True, correction=correction)
|