Spaces:
Running
Running
| import difflib | |
| from dataclasses import dataclass | |
| from typing import Any, Dict, List, Optional, Tuple | |
| from core.text_utils import clean_token | |
| class AlignItem: | |
| ref_word: Optional[str] | |
| hyp_word: Optional[str] | |
| status: str | |
| ref_start: Optional[float] | |
| ref_end: Optional[float] | |
| hyp_start: Optional[float] | |
| hyp_end: Optional[float] | |
| suggestion: str | |
| severity: Optional[float] = None | |
| confidence: Optional[float] = None | |
| decision: Optional[str] = None | |
| evidence: Optional[str] = None | |
| issue_type: Optional[str] = None | |
| def align_words( | |
| ref_words: List[Dict[str, Any]], | |
| hyp_words: List[Dict[str, Any]], | |
| suggest_for_word, | |
| ) -> List[AlignItem]: | |
| ref_items = [(w, clean_token(w["word"])) for w in ref_words] | |
| hyp_items = [(w, clean_token(w["word"])) for w in hyp_words] | |
| ref_items = [(w, t) for (w, t) in ref_items if t] | |
| hyp_items = [(w, t) for (w, t) in hyp_items if t] | |
| ref_tokens = [t for _, t in ref_items] | |
| hyp_tokens = [t for _, t in hyp_items] | |
| def dp_align(ref_slice: List[Tuple[Dict[str, Any], str]], hyp_slice: List[Tuple[Dict[str, Any], str]]): | |
| if not ref_slice and not hyp_slice: | |
| return [] | |
| if not ref_slice: | |
| return [ | |
| AlignItem( | |
| ref_word=None, | |
| hyp_word=hyp["word"], | |
| status="insert", | |
| ref_start=None, | |
| ref_end=None, | |
| hyp_start=hyp["start"], | |
| hyp_end=hyp["end"], | |
| suggestion=suggest_for_word(hyp["word"], "insert"), | |
| issue_type="content", | |
| ) | |
| for hyp, _ in hyp_slice | |
| ] | |
| if not hyp_slice: | |
| return [ | |
| AlignItem( | |
| ref_word=ref["word"], | |
| hyp_word=None, | |
| status="delete", | |
| ref_start=ref["start"], | |
| ref_end=ref["end"], | |
| hyp_start=None, | |
| hyp_end=None, | |
| suggestion=suggest_for_word(ref["word"], "delete"), | |
| issue_type="content", | |
| ) | |
| for ref, _ in ref_slice | |
| ] | |
| ref_toks = [t for _, t in ref_slice] | |
| hyp_toks = [t for _, t in hyp_slice] | |
| n, m = len(ref_toks), len(hyp_toks) | |
| dp = [[0] * (m + 1) for _ in range(n + 1)] | |
| back = [[""] * (m + 1) for _ in range(n + 1)] | |
| for i in range(1, n + 1): | |
| dp[i][0] = i | |
| back[i][0] = "del" | |
| for j in range(1, m + 1): | |
| dp[0][j] = j | |
| back[0][j] = "ins" | |
| for i in range(1, n + 1): | |
| for j in range(1, m + 1): | |
| cost = 0 if ref_toks[i - 1] == hyp_toks[j - 1] else 1 | |
| subs = dp[i - 1][j - 1] + cost | |
| dele = dp[i - 1][j] + 1 | |
| ins = dp[i][j - 1] + 1 | |
| best = min(subs, dele, ins) | |
| dp[i][j] = best | |
| if best == subs: | |
| back[i][j] = "eq" if cost == 0 else "sub" | |
| elif best == dele: | |
| back[i][j] = "del" | |
| else: | |
| back[i][j] = "ins" | |
| items: List[AlignItem] = [] | |
| i, j = n, m | |
| while i > 0 or j > 0: | |
| op = back[i][j] | |
| if op in ("eq", "sub"): | |
| ref = ref_slice[i - 1][0] | |
| hyp = hyp_slice[j - 1][0] | |
| status = "equal" if op == "eq" else "replace" | |
| items.append( | |
| AlignItem( | |
| ref_word=ref["word"], | |
| hyp_word=hyp["word"], | |
| status=status, | |
| ref_start=ref["start"], | |
| ref_end=ref["end"], | |
| hyp_start=hyp["start"], | |
| hyp_end=hyp["end"], | |
| suggestion=suggest_for_word(ref["word"], status), | |
| issue_type="content" if status != "equal" else None, | |
| ) | |
| ) | |
| i -= 1 | |
| j -= 1 | |
| elif op == "del": | |
| ref = ref_slice[i - 1][0] | |
| items.append( | |
| AlignItem( | |
| ref_word=ref["word"], | |
| hyp_word=None, | |
| status="delete", | |
| ref_start=ref["start"], | |
| ref_end=ref["end"], | |
| hyp_start=None, | |
| hyp_end=None, | |
| suggestion=suggest_for_word(ref["word"], "delete"), | |
| issue_type="content", | |
| ) | |
| ) | |
| i -= 1 | |
| else: | |
| hyp = hyp_slice[j - 1][0] | |
| items.append( | |
| AlignItem( | |
| ref_word=None, | |
| hyp_word=hyp["word"], | |
| status="insert", | |
| ref_start=None, | |
| ref_end=None, | |
| hyp_start=hyp["start"], | |
| hyp_end=hyp["end"], | |
| suggestion=suggest_for_word(hyp["word"], "insert"), | |
| issue_type="content", | |
| ) | |
| ) | |
| j -= 1 | |
| items.reverse() | |
| return items | |
| if not ref_tokens and not hyp_tokens: | |
| return [] | |
| matcher = difflib.SequenceMatcher(a=ref_tokens, b=hyp_tokens, autojunk=False) | |
| blocks = [b for b in matcher.get_matching_blocks() if b.size > 0] | |
| if not blocks: | |
| return dp_align(ref_items, hyp_items) | |
| aligned: List[AlignItem] = [] | |
| ref_i = 0 | |
| hyp_i = 0 | |
| for block in blocks: | |
| if ref_i < block.a or hyp_i < block.b: | |
| aligned.extend(dp_align(ref_items[ref_i:block.a], hyp_items[hyp_i:block.b])) | |
| for k in range(block.size): | |
| ref = ref_items[block.a + k][0] | |
| hyp = hyp_items[block.b + k][0] | |
| aligned.append( | |
| AlignItem( | |
| ref_word=ref["word"], | |
| hyp_word=hyp["word"], | |
| status="equal", | |
| ref_start=ref["start"], | |
| ref_end=ref["end"], | |
| hyp_start=hyp["start"], | |
| hyp_end=hyp["end"], | |
| suggestion="", | |
| ) | |
| ) | |
| ref_i = block.a + block.size | |
| hyp_i = block.b + block.size | |
| if ref_i < len(ref_items) or hyp_i < len(hyp_items): | |
| aligned.extend(dp_align(ref_items[ref_i:], hyp_items[hyp_i:])) | |
| return aligned | |
| def compute_wer(items: List[AlignItem]) -> float: | |
| subs = sum(1 for x in items if x.status == "replace") | |
| ins = sum(1 for x in items if x.status == "insert") | |
| dele = sum(1 for x in items if x.status == "delete") | |
| ref_len = sum(1 for x in items if x.ref_word is not None) | |
| if ref_len == 0: | |
| return 1.0 | |
| return (subs + ins + dele) / ref_len | |