feat: Aufgaben mit Abbildungen (docling-Markdown), mit Gegenprobe beim Ablesen

Eingebettete Base64-Bilder und Bilddateien werden aus dem Aufgabentext gelöst und nur
der Verstehensstufe als Bild übergeben; sie überträgt jede Abbildung in Text. Eine
zweite, unabhängige Lesung wird Zahl für Zahl verglichen (bei Abweichung entscheidet
eine dritte, sonst reliable=False). Alle weiteren Stufen rechnen mit der Übertragung,
der Report zeigt Original und Übertragung nebeneinander. LLM_MODEL_UNDERSTANDING
wählt das Bildmodell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-17 17:52:52 +02:00
commit b07dd16143

View file

@ -4,6 +4,7 @@ error -> löst in run_pipeline eine Korrekturrunde aus
warning -> wird nur berichtet (Heuristiken mit möglichen Fehlalarmen)
"""
import re
from collections import Counter
from typing import Iterable, List, Set
from core.model_schema import (
@ -205,6 +206,44 @@ def check_understanding(understanding, task: str) -> List[CheckIssue]:
return issues
def _numbers_multiset(text: str) -> Counter:
"""Zahlen eines Textes mit Häufigkeit – eine Kante mit Bewertung 3 zweimal ist nicht dasselbe wie einmal."""
zaehler = Counter()
for roh in NUMBER_PATTERN.findall(text or ""):
try:
zaehler[normalize_number(roh)] += 1
except ValueError:
continue
return zaehler
def check_figure_readings(first, second) -> List[CheckIssue]:
"""Zwei unabhängige Übertragungen derselben Abbildungen vergleichen – Zahl für Zahl, ohne LLM.
Ein Bildmodell kann eine Pfeilbewertung dem falschen Pfeil zuordnen oder eine Ziffer verlesen; eine einzige
Lesung wäre dann ungeprüft die Datengrundlage jeder weiteren Stufe. Verglichen werden die Zahlen je Abbildung
samt Häufigkeit. Richtungsfehler bei gleichen Zahlen fängt das nicht – dafür steht das Original im Report.
"""
issues = []
erste = {f.id: f for f in first}
zweite = {f.id: f for f in second}
for kennung in sorted(set(erste) | set(zweite)):
if kennung not in erste or kennung not in zweite:
issues.append(CheckIssue(severity=CheckSeverity.ERROR, code="FIGURE_READINGS_DIFFER",
message=f"{kennung} wurde nur in einer von zwei Lesungen übertragen."))
continue
a = _numbers_multiset(erste[kennung].transcription)
b = _numbers_multiset(zweite[kennung].transcription)
if a != b:
nur_a = sorted((a - b).elements(), key=float)
nur_b = sorted((b - a).elements(), key=float)
issues.append(CheckIssue(
severity=CheckSeverity.ERROR, code="FIGURE_READINGS_DIFFER",
message=f"{kennung} uneinheitlich gelesen – nur in Lesung 1: {', '.join(nur_a) or '–'}; "
f"nur in Lesung 2: {', '.join(nur_b) or '–'}."))
return issues
def _strip_scripts(text: str) -> str:
"""Entfernt Indizes/Exponenten: x_{i,j} -> x, C^{prod}_{p} -> C, x_1 -> x."""
text = text.replace(r"\_", "_")