import re import unicodedata import json def normalize_text(text: str) -> str: if not text: return "" text = text.lower().strip() # Replace German umlauts text = text.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss") # Remove accents nfkd = unicodedata.normalize("NFKD", text) text = "".join([c for c in nfkd if not unicodedata.combining(c)]) # Remove special punctuation like quotes, dots, dashes, commas text = re.sub(r"[^\w\s]", "", text) # Collapse whitespace text = re.sub(r"\s+", " ", text).strip() return text def levenshtein_distance(s1: str, s2: str) -> int: if len(s1) < len(s2): return levenshtein_distance(s2, s1) if len(s2) == 0: return len(s1) previous_row = list(range(len(s2) + 1)) for i, c1 in enumerate(s1): current_row = [i + 1] for j, c2 in enumerate(s2): insertions = previous_row[j + 1] + 1 deletions = current_row[j] + 1 substitutions = previous_row[j] + (c1 != c2) current_row.append(min(insertions, deletions, substitutions)) previous_row = current_row return previous_row[-1] def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None = None) -> bool: clean_user = normalize_text(user_input) if not clean_user: return False targets = [normalize_text(canonical)] if accepted_raw: try: parsed = json.loads(accepted_raw) if isinstance(parsed, list): targets.extend([normalize_text(str(x)) for x in parsed]) except Exception: # Fallback comma-separated for part in accepted_raw.split(","): part_clean = normalize_text(part) if part_clean: targets.append(part_clean) for target in targets: if not target: continue if clean_user == target: return True # For short answers (1-3 chars, e.g. numbers or abbreviations), require exact match if len(target) <= 3: continue # For medium words (4-6 chars), allow max 1 typo max_allowed = 1 if len(target) <= 6 else 2 dist = levenshtein_distance(clean_user, target) if dist <= max_allowed: return True return False