"""Dynamic Accent Flash text front-end. from frontend import FrontEnd fe = FrontEnd(diac_model="models/diacritics", mt=True) fe("The meeting is at 3:30 on 21/10/2026.", mode="codeswitch") # tags kept, numbers verbalised fe("Ojo naa dara pupo", mode="native", lang="yor") # adds Yorùbá tone marks/underdots fe("Please come home early today.", mode="translate", lang="ibo") # MADLAD-400 eng/pcm -> ibo fe("I go call you tomorrow", mode="accent", lang="ibo") # English/Pidgin words, Igbo accent Modes codeswitch : input already tagged … … (or untagged = one language `lang`); each segment is normalised in its own language; Yorùbá/Igbo segments that carry no marks are diacritised. native : the whole text is one language (`lang`); normalise + diacritise. translate : `lang` is the target (ibo|yor|hau); the input (English or Pidgin) is translated first with MADLAD-400-3B-MT (Apache-2.0), then normalised + diacritised (MADLAD Yorùbá is often unmarked). accent : keep the words as written but read them with the `lang` accent (whole text tagged ), i.e. "English words with a native accent". Numbers are verbalised in English. Output: a tagged string for infer.py / flash_lm.py. """ import re import unicodedata try: from .normalize import normalize except ImportError: # run as a script from normalize import normalize SEG = re.compile(r"<(\w{3})>(.*?)", re.S) def _strip(s): return "".join(c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn") def unmarked(text, thresh=0.05): w = re.findall(r"\w+", unicodedata.normalize("NFC", text)) if not w: return False return sum(_strip(x) != unicodedata.normalize("NFC", x) for x in w) / len(w) < thresh class FrontEnd: def __init__(self, diac_model=None, mt=False, device="cuda"): self.diac_dir, self.device = diac_model, device self._d = self._mt = None self.use_mt = mt # ---- diacritics (ByT5-small fine-tune, word-guarded) def diacritise(self, text, lang): if lang not in ("yor", "ibo") or not self.diac_dir: return text import torch from transformers import AutoTokenizer, T5ForConditionalGeneration if self._d is None: self._d = (AutoTokenizer.from_pretrained(self.diac_dir), T5ForConditionalGeneration.from_pretrained(self.diac_dir).to(self.device).eval()) tok, m = self._d src = unicodedata.normalize("NFC", _strip(text)) x = tok([f"{lang}: {src}"], return_tensors="pt").to(self.device) with torch.no_grad(): y = m.generate(**x, max_length=int(x.input_ids.shape[1] * 1.6) + 16) hyp = unicodedata.normalize("NFC", tok.decode(y[0], skip_special_tokens=True)) sw, hw = src.split(), hyp.split() if len(sw) != len(hw): return text return " ".join(h if unicodedata.normalize("NFC", _strip(h)) == s else s for s, h in zip(sw, hw)) # ---- MT (MADLAD-400-3B-MT) def translate(self, text, tgt): import torch from transformers import T5ForConditionalGeneration, T5Tokenizer if self._mt is None: self._mt = (T5Tokenizer.from_pretrained("google/madlad400-3b-mt"), T5ForConditionalGeneration.from_pretrained("google/madlad400-3b-mt", torch_dtype=torch.bfloat16).to(self.device).eval()) tok, m = self._mt code = {"ibo": "ig", "yor": "yo", "hau": "ha", "eng": "en"}[tgt] out = [] for sent in re.split(r"(?<=[.!?])\s+", text.strip()): if not sent: continue x = tok([f"<2{code}> {sent}"], return_tensors="pt").to(self.device) with torch.no_grad(): y = m.generate(**x, max_new_tokens=256, num_beams=4) out.append(tok.decode(y[0], skip_special_tokens=True)) return " ".join(out) def _seg(self, lang, s): s = normalize(s, lang) if lang in ("yor", "ibo") and unmarked(s): s = self.diacritise(s, lang) return f"<{lang}>{s}" def __call__(self, text, mode="codeswitch", lang="eng"): text = unicodedata.normalize("NFC", text).strip() if mode == "translate": if lang not in ("ibo", "yor", "hau"): raise ValueError("translate mode needs lang in ibo|yor|hau") plain = re.sub(r"", "", text) return self._seg(lang, self.translate(normalize(plain, "eng"), lang)) if mode == "accent": plain = re.sub(r"", "", text) return f"<{lang}>{normalize(plain, 'eng')}" if mode == "native" or not SEG.search(text): return self._seg(lang, re.sub(r"", "", text)) parts, pos = [], 0 for m in SEG.finditer(text): if text[pos:m.start()].strip(): parts.append(self._seg(lang, text[pos:m.start()])) parts.append(self._seg(m.group(1), m.group(2))) pos = m.end() if text[pos:].strip(): parts.append(self._seg(lang, text[pos:])) return " ".join(parts) if __name__ == "__main__": import argparse ap = argparse.ArgumentParser() ap.add_argument("text"); ap.add_argument("--mode", default="codeswitch"); ap.add_argument("--lang", default="eng") ap.add_argument("--diac_model", default=None); ap.add_argument("--cpu", action="store_true") a = ap.parse_args() print(FrontEnd(a.diac_model, device="cpu" if a.cpu else "cuda")(a.text, a.mode, a.lang))