import difflib import re from typing import List, Tuple def preprocess(text: str) -> List[str]: return re.sub(r"[^\w\s]", "", text.lower()).split() def tokenize_raw_text(text: str) -> List[str]: return re.findall(r"\b\w+(?:'\w+)?\b|[^\w\s]", text) def extract_segment_with_punct(raw_words: List[str], start_index: int, word_count: int) -> Tuple[ List[Tuple[str, bool]], int]: segment = [] word_seen = 0 i = start_index while word_seen < word_count and i < len(raw_words): word = raw_words[i] if re.match(r"\w+", word): # це слово punct = "" i += 1 while i < len(raw_words) and not re.match(r"\w+", raw_words[i]): punct += raw_words[i] i += 1 segment.append((word + punct, True)) word_seen += 1 else: segment.append((word, False)) i += 1 return segment, i def style_word(word: str, correct: bool) -> str: color = "#e6ffe6" if correct else "#ffe6e6" text_color = "#006600" if correct else "#990000" content = word if correct else "..." return ( f"{content}" ) def highlight_fuzzy_diff(user_text: str, original_text: str) -> Tuple[str, int]: original_words_raw = tokenize_raw_text(original_text) original_words_clean = preprocess(original_text) user_words_clean = preprocess(user_text) matcher = difflib.SequenceMatcher(None, user_words_clean, original_words_clean) highlighted = [] correct_count = 0 total_count = 0 original_pointer = 0 for op, i1, i2, j1, j2 in matcher.get_opcodes(): word_count = j2 - j1 segment_raw, original_pointer = extract_segment_with_punct(original_words_raw, original_pointer, word_count) for token, is_word in segment_raw: if is_word: total_count += 1 is_correct = op == "equal" if is_correct: correct_count += 1 highlighted.append(style_word(token, is_correct)) else: highlighted.append(token) score_percent = round((correct_count / total_count) * 100) if total_count > 0 else 0 return " ".join(highlighted), score_percent