fix: ship bhc/casual.py (translit2.py imports it; the python/ tree could not be imported)
Browse files- python/bhc/casual.py +174 -0
python/bhc/casual.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Casual / chat-romanization keys (AksharaLP v2).
|
| 2 |
+
|
| 3 |
+
Two lossy keys are computed for a roman token and for a Devanagari word; a
|
| 4 |
+
casual spelling matches the intended word when the keys collide.
|
| 5 |
+
K1: consonant classes (aspiration, nukta, sibilants merged), vowel classes
|
| 6 |
+
(length merged, ai->e, au->o), every 'a'/schwa removed except word
|
| 7 |
+
initial, nasal dropped, repeats collapsed.
|
| 8 |
+
K2: K1 with all vowels removed (chat consonant-only spellings: bht, thk).
|
| 9 |
+
The index is built at load time from the lexicon (0 bytes stored).
|
| 10 |
+
Same algorithm is ported 1:1 to JS (release/runtime) - keep in sync.
|
| 11 |
+
"""
|
| 12 |
+
import re
|
| 13 |
+
|
| 14 |
+
ASP = set("kgjtdpbc")
|
| 15 |
+
# Devanagari -> key class (nukta ignored except ड़/ढ़ -> R)
|
| 16 |
+
_DC = {}
|
| 17 |
+
for chars, k in [("कख", "k"), ("गघ", "g"), ("चछ", "c"), ("जझ", "j"), ("टठतथ", "t"),
|
| 18 |
+
("डढदध", "d"), ("णनऩ", "n"), ("पफ", "p"), ("बभ", "b"), ("म", "m"),
|
| 19 |
+
("य", "y"), ("रऱ", "r"), ("लळ", "l"), ("व", "v"), ("शषस", "s"), ("ह", "h")]:
|
| 20 |
+
for c in chars: _DC[c] = k
|
| 21 |
+
for c, k in zip("\u0958\u0959\u095a\u095b\u095c\u095d\u095e\u095f", "kkgjRRpy"): _DC[c] = k
|
| 22 |
+
_DV = {}
|
| 23 |
+
for chars, k in [("अआा", "a"), ("इईिी", "i"), ("उऊुू", "u"), ("एऐेैॅऍ", "e"), ("ओऔऑोौॉ", "o"), ("ऋृ", "r")]:
|
| 24 |
+
for c in chars: _DV[c] = k
|
| 25 |
+
VOW = set("aeiou")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _fin(s):
|
| 29 |
+
# drop non-initial a, collapse repeats
|
| 30 |
+
out = []
|
| 31 |
+
for i, ch in enumerate(s):
|
| 32 |
+
if ch == "a" and i > 0: continue
|
| 33 |
+
if out and out[-1] == ch: continue
|
| 34 |
+
out.append(ch)
|
| 35 |
+
return "".join(out)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def k2_of(k1):
|
| 39 |
+
s = k1[0] + "".join(c for c in k1[1:] if c not in VOW) if k1 else ""
|
| 40 |
+
if s and s[0] in VOW: s = s[1:] or s
|
| 41 |
+
out = []
|
| 42 |
+
for ch in s:
|
| 43 |
+
if out and out[-1] == ch: continue
|
| 44 |
+
out.append(ch)
|
| 45 |
+
return "".join(out)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def dev_keys(w):
|
| 49 |
+
"""K1 keys of a Devanagari word (2 if it has ड़/ढ़: as d and as r)."""
|
| 50 |
+
raw = []
|
| 51 |
+
for i, ch in enumerate(w):
|
| 52 |
+
if ch == "\u093c":
|
| 53 |
+
if raw and raw[-1] == "d" and i > 0 and w[i - 1] in "डढ": raw[-1] = "R"
|
| 54 |
+
continue
|
| 55 |
+
if ch in _DC: raw.append(_DC[ch])
|
| 56 |
+
elif ch in _DV: raw.append(_DV[ch])
|
| 57 |
+
s = "".join(raw)
|
| 58 |
+
if not s: return []
|
| 59 |
+
ks = {_fin(s.replace("R", "d")), _fin(s.replace("R", "r"))}
|
| 60 |
+
return sorted(k for k in ks if k)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _norm(t):
|
| 64 |
+
t = t.lower()
|
| 65 |
+
t = re.sub(r"[^a-z]", "", t)
|
| 66 |
+
return t
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def rom_keys(t, cap=12):
|
| 70 |
+
"""K1 keys of a roman token, expanded over ambiguous casual rules."""
|
| 71 |
+
t = _norm(t)
|
| 72 |
+
if not t: return [], []
|
| 73 |
+
t = re.sub(r"(.)\1{2,}", r"\1", t) # emphasis repeats
|
| 74 |
+
# deterministic letter merges
|
| 75 |
+
t = t.replace("chh", "c").replace("cch", "c").replace("ch", "c").replace("sh", "s")
|
| 76 |
+
t = t.replace("x", "ks").replace("q", "k").replace("z", "j").replace("f", "p").replace("w", "v")
|
| 77 |
+
# (unit list) each position: list of alternatives
|
| 78 |
+
alts = []
|
| 79 |
+
i = 0; L = len(t)
|
| 80 |
+
while i < L:
|
| 81 |
+
c = t[i]; nx = t[i + 1] if i + 1 < L else ""
|
| 82 |
+
prev = t[i - 1] if i > 0 else ""
|
| 83 |
+
if c == "h" and prev in ASP and i > 0:
|
| 84 |
+
alts.append(("", "h")); i += 1; continue # aspiration / separate h
|
| 85 |
+
if nx and c in "ae" and nx in "iy" and (i + 2 >= L or t[i + 2] not in VOW):
|
| 86 |
+
if c == "a" and nx == "i": alts.append(("e", "ai")); i += 2; continue
|
| 87 |
+
if c == "a" and nx == "y" and i + 2 >= L: alts.append(("e",)); i += 2; continue
|
| 88 |
+
if c == "e" and nx == "y" and i + 2 >= L: alts.append(("e",)); i += 2; continue
|
| 89 |
+
if nx and c in "ao" and nx in "uvw" and (i + 2 >= L or t[i + 2] not in VOW) and c + nx != "av":
|
| 90 |
+
alts.append(("o", c + "u")); i += 2; continue
|
| 91 |
+
if c == "o" and nx == "u": alts.append(("o", "u")); i += 2; continue
|
| 92 |
+
if c == "e" and nx == "e": alts.append(("i",)); i += 2; continue
|
| 93 |
+
if c == "o" and nx == "o": alts.append(("u", "o")); i += 2; continue
|
| 94 |
+
if c == "e" and nx == "i" and (i + 2 >= L or t[i + 2] not in VOW): alts.append(("e",)); i += 2; continue
|
| 95 |
+
if c == "e" and nx == "h" and i + 2 < L and t[i + 2] not in VOW and i > 0: alts.append(("e", "")); i += 1; continue
|
| 96 |
+
if nx and c == "i" and nx in "aeou" and i > 0: alts.append(("y", "i")); i += 1; continue
|
| 97 |
+
if c == "y" and i == L - 1 and prev and prev not in VOW: alts.append(("e", "i")); i += 1; continue
|
| 98 |
+
if c == "n" and prev in VOW and i > 0 and (i == L - 1 or (nx and nx not in VOW and nx != "y")):
|
| 99 |
+
alts.append(("n", "")); i += 1; continue
|
| 100 |
+
if c in "iu" and prev == "r" and i >= 2 and t[i - 2] not in VOW:
|
| 101 |
+
alts.append((c, "")); i += 1; continue
|
| 102 |
+
alts.append((c,)); i += 1
|
| 103 |
+
outs = [""]
|
| 104 |
+
for a in alts:
|
| 105 |
+
nxt = []
|
| 106 |
+
for o in outs:
|
| 107 |
+
for x in a: nxt.append(o + x)
|
| 108 |
+
outs = list(dict.fromkeys(nxt))[:64]
|
| 109 |
+
k1 = []; seen = set()
|
| 110 |
+
for o in outs:
|
| 111 |
+
k = _fin(o)
|
| 112 |
+
if k and k not in seen: seen.add(k); k1.append(k)
|
| 113 |
+
k1 = k1[:cap]
|
| 114 |
+
k2 = list(dict.fromkeys(k2_of(k) for k in k1 if k2_of(k)))
|
| 115 |
+
return k1, k2
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# Irregular chat abbreviations (not derivable by rules). Values: candidate natives in prior order.
|
| 119 |
+
ABBR = {
|
| 120 |
+
"h": ["है", "हैं"], "hy": ["है"], "hn": ["हाँ", "हैं"], "hm": ["हम", "हम्म"],
|
| 121 |
+
"k": ["के", "की", "ओके"], "ki": ["की", "कि"], "n": ["ना", "न"], "ni": ["नहीं", "नी"], "nai": ["नहीं", "नई"],
|
| 122 |
+
"nhi": ["नहीं"], "nhe": ["नहीं"], "nahi": ["नहीं"], "nai": ["नहीं", "नई"],
|
| 123 |
+
"kr": ["कर"], "krna": ["करना"], "kro": ["करो"], "tm": ["तुम"], "tu": ["तू"], "mai": ["मैं", "में"],
|
| 124 |
+
"me": ["में", "मैं"], "m": ["मैं", "में"], "thk": ["ठीक"], "tk": ["ठीक", "तक"],
|
| 125 |
+
"yr": ["यार"], "yrr": ["यार"], "plz": ["प्लीज़"], "pls": ["प्लीज़"], "plzz": ["प्लीज़"],
|
| 126 |
+
"tq": ["थैंक यू"], "ty": ["थैंक यू"], "thx": ["थैंक्स"], "thnx": ["थैंक्स"], "thnks": ["थैंक्स"],
|
| 127 |
+
"gn": ["गुड नाइट"], "gm": ["गुड मॉर्निंग"], "ok": ["ओके"], "okk": ["ओके"], "kk": ["ओके"],
|
| 128 |
+
"sry": ["सॉरी"], "bro": ["ब्रो"], "msg": ["मैसेज"], "bday": ["बर्थडे"], "clg": ["कॉलेज"],
|
| 129 |
+
"ofc": ["ऑफ़िस"], "pic": ["पिक"], "bhai": ["भाई"], "bht": ["बहुत"], "bhot": ["बहुत"], "bohot": ["बहुत"],
|
| 130 |
+
"pta": ["पता"], "kb": ["कब"], "jb": ["जब"], "sb": ["सब"], "mtlb": ["मतलब"], "kch": ["कुछ"], "kuch": ["कुछ"],
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class CasualIndex:
|
| 135 |
+
"""K1/K2 -> lexicon ids sorted by frequency (top `top_n` lexicon words only)."""
|
| 136 |
+
|
| 137 |
+
def __init__(self, words, uni=None, top_n=300000, per_key=8):
|
| 138 |
+
order = range(len(words))
|
| 139 |
+
if uni is not None:
|
| 140 |
+
u = uni if callable(uni) else uni.__getitem__
|
| 141 |
+
order = sorted(order, key=lambda i: (-u(i), i))
|
| 142 |
+
self.k1 = {}; self.k2 = {}
|
| 143 |
+
for r, i in enumerate(order):
|
| 144 |
+
if r >= top_n: break
|
| 145 |
+
for k in dev_keys(words[i]):
|
| 146 |
+
b = self.k1.setdefault(k, [])
|
| 147 |
+
if len(b) < per_key and i not in b: b.append(i)
|
| 148 |
+
k2 = k2_of(k)
|
| 149 |
+
if k2:
|
| 150 |
+
b2 = self.k2.setdefault(k2, [])
|
| 151 |
+
if len(b2) < per_key and i not in b2: b2.append(i)
|
| 152 |
+
|
| 153 |
+
def lookup(self, roman, n1=6, n2=4):
|
| 154 |
+
k1, k2 = rom_keys(roman)
|
| 155 |
+
r1 = []; r2 = []
|
| 156 |
+
for k in k1:
|
| 157 |
+
for i in self.k1.get(k, ()):
|
| 158 |
+
if i not in r1: r1.append(i)
|
| 159 |
+
for k in k2:
|
| 160 |
+
for i in self.k2.get(k, ()):
|
| 161 |
+
if i not in r2: r2.append(i)
|
| 162 |
+
return r1[:n1 * 2], r2[:n2 * 2]
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def squeeze_in(t, mode=0):
|
| 166 |
+
"""Emphasis-repeat normalization of the model input. mode 0: off; 1: 3+ -> 1; 2: 3+ -> 2 for a/e/o, 1 otherwise."""
|
| 167 |
+
if not mode: return t
|
| 168 |
+
if mode == 1: return re.sub(r"([a-z])\1{2,}", r"\1", t)
|
| 169 |
+
return re.sub(r"([a-z])\1{2,}", lambda m: m.group(1) * (2 if m.group(1) in "aeo" else 1), t)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def squeeze(t):
|
| 173 |
+
"""Normalize emphasis repeats (3+ same letters -> keep 1, e.g. pleaseee->please)."""
|
| 174 |
+
return re.sub(r"([a-zA-Z])\1{2,}", r"\1", t)
|