| 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"<span style='background-color:{color}; color:{text_color}; padding:4px 10px; " |
| f"margin:4px; border-radius:999px; font-weight:500; display:inline-block;'>{content}</span>" |
| ) |
|
|
|
|
| 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 |
|
|