NightPrince commited on
Commit
f1dfd46
·
verified ·
1 Parent(s): 3862eca

Add diacritizer wrapper

Browse files
Files changed (1) hide show
  1. diacritize.py +91 -0
diacritize.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Arabic diacritization via vendored CATT (encoder-decoder), punctuation-preserving.
2
+
3
+ CATT strips punctuation before diacritizing. For TTS we must keep punctuation (it drives
4
+ prosody), so we diacritize the full sentence for context, then map the diacritized words
5
+ back onto the original token positions, leaving punctuation/spacing untouched.
6
+
7
+ Usage:
8
+ from tts.text.diacritize import Diacritizer
9
+ d = Diacritizer() # loads model on GPU if available
10
+ d.diacritize_texts(["ما أجمل الصلاة"]) # -> ["مَا أَجْمَلُ الصَّلَاةِ"]
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ import torch
20
+
21
+ _CATT_DIR = Path(__file__).parent / "catt"
22
+ _DEFAULT_CKPT = Path("models/catt/best_ed_mlm_ns_epoch_178.pt")
23
+
24
+ # Characters that belong to an Arabic "word": letters + harakat + super/wasla alef + tatweel.
25
+ _WORD = r"ء-يً-ْٰٱـ"
26
+ _TOKEN_RE = re.compile(rf"[{_WORD}]+|[^{_WORD}]+")
27
+ _IS_WORD_RE = re.compile(rf"[{_WORD}]")
28
+
29
+
30
+ class Diacritizer:
31
+ def __init__(self, ckpt: str | Path | None = None, device: str | None = None,
32
+ max_seq_len: int = 1024) -> None:
33
+ if str(_CATT_DIR) not in sys.path:
34
+ sys.path.insert(0, str(_CATT_DIR))
35
+ from ed_pl import TashkeelModel # noqa: E402 (vendored CATT)
36
+ from tashkeel_tokenizer import TashkeelTokenizer # noqa: E402
37
+ from utils import remove_non_arabic # noqa: E402
38
+
39
+ self._clean = remove_non_arabic
40
+ self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
41
+ self.tokenizer = TashkeelTokenizer()
42
+ self.model = TashkeelModel(
43
+ self.tokenizer, max_seq_len=max_seq_len, n_layers=3, learnable_pos_emb=False
44
+ )
45
+ ckpt = Path(ckpt) if ckpt else _DEFAULT_CKPT
46
+ try:
47
+ state = torch.load(ckpt, map_location=self.device, weights_only=True)
48
+ except Exception: # noqa: BLE001 (trusted, user-authorized checkpoint)
49
+ state = torch.load(ckpt, map_location=self.device, weights_only=False)
50
+ self.model.load_state_dict(state)
51
+ self.model.eval().to(self.device)
52
+
53
+ def _reinsert(self, original: str, diac_sentence: str) -> str:
54
+ """Put CATT's diacritized words back onto original token positions."""
55
+ diac_words = diac_sentence.split()
56
+ out, wi = [], 0
57
+ for tok in _TOKEN_RE.findall(original):
58
+ if _IS_WORD_RE.match(tok):
59
+ if wi < len(diac_words):
60
+ out.append(diac_words[wi])
61
+ wi += 1
62
+ else:
63
+ out.append(tok) # ran out — keep original (safety)
64
+ else:
65
+ out.append(tok) # punctuation / whitespace preserved verbatim
66
+ # If word counts disagreed, alignment is unsafe -> signal caller to fall back.
67
+ if wi != len(diac_words):
68
+ return ""
69
+ return "".join(out)
70
+
71
+ def diacritize_texts(self, texts: list[str], batch_size: int = 16,
72
+ verbose: bool = False) -> list[str]:
73
+ cleaned = [self._clean(t) for t in texts]
74
+ diac = self.model.do_tashkeel_batch(cleaned, batch_size, verbose)
75
+ results = []
76
+ for orig, ds in zip(texts, diac):
77
+ merged = self._reinsert(orig, ds)
78
+ if not merged: # fallback: diacritize per punctuation-delimited phrase
79
+ merged = self._phrasewise(orig, batch_size)
80
+ results.append(merged)
81
+ return results
82
+
83
+ def _phrasewise(self, text: str, batch_size: int) -> str:
84
+ """Fallback: split on non-word separators, diacritize each Arabic phrase."""
85
+ parts = _TOKEN_RE.findall(text)
86
+ arabic_idx = [i for i, p in enumerate(parts) if _IS_WORD_RE.match(p)]
87
+ phrases = [parts[i] for i in arabic_idx]
88
+ diac = self.model.do_tashkeel_batch([self._clean(p) for p in phrases], batch_size, False)
89
+ for i, d in zip(arabic_idx, diac):
90
+ parts[i] = d if d.strip() else parts[i]
91
+ return "".join(parts)