rp-demo / core /alignment.py
Difficult-Burger's picture
add core modules for alignment and pronunciation
7f04ca4 verified
Raw
History Blame Contribute Delete
7.05 kB
import difflib
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
from core.text_utils import clean_token
@dataclass
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