Token Classification
Transformers
Safetensors
Malagasy
xlm-roberta
punctuation-restoration
truecasing
malagasy
asr-postprocessing
Instructions to use Flo976/malagasy-punctuation with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Flo976/malagasy-punctuation with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="Flo976/malagasy-punctuation")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("Flo976/malagasy-punctuation") model = AutoModelForTokenClassification.from_pretrained("Flo976/malagasy-punctuation", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download punctuate.py from Flo976/malagasy-punctuation: direct link, hf CLI and curl.
- Browser
- Download file 8.65 kB
-
https://huggingface.co/Flo976/malagasy-punctuation/resolve/main/punctuate.py
- Command line
-
hf download hf://Flo976/malagasy-punctuation/punctuate.py
-
curl -L -o punctuate.py https://huggingface.co/Flo976/malagasy-punctuation/resolve/main/punctuate.py
8.65 kB
| #!/usr/bin/env python | |
| """Restauration des majuscules et de la ponctuation pour la sortie ASR malgache. | |
| from punctuate import restore | |
| restore("lanonana ara-panambadiana iray ahitana karazan'olona mandihy") | |
| -> "Lanonana ara-panambadiana iray ahitana karazan'olona mandihy." | |
| Garantie : normalize(sortie).split() == normalize(entree).split() (assert). | |
| Les mots ne sont jamais changes ; seules la casse et la ponctuation qui suit | |
| chaque mot sont predites (plus, en option, la remise des lettres accentuees | |
| qu'efface normalize(), ex. "tan na" -> "tanàna", qui respecte la meme garantie). | |
| Modele : XLM-R base (Davlan/xlm-roberta-base-finetuned-malagasy) + une tete de | |
| classification de jetons combinee (4 casses x 7 ponctuations), etiquette lue | |
| sur le premier sous-jeton de chaque mot. Textes longs : fenetres glissantes. | |
| CLI : | |
| python punctuate.py "texte en minuscules" # restaure | |
| python punctuate.py --bench 100 --threads 2 # latence CPU | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import time | |
| import numpy as np | |
| import torch | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, HERE) | |
| from common import CASES, PUNCTS, normalize, render # noqa: E402 | |
| DEFAULT_MODEL = os.environ.get("MG_PUNCT_DIR", HERE) | |
| NP = len(PUNCTS) | |
| def windows(n, size, step): | |
| """Fenetres [a, b) couvrant n mots ; chaque mot sera lu dans la fenetre ou il est le plus central.""" | |
| if n <= size: | |
| return [(0, n)] | |
| out, a = [], 0 | |
| while True: | |
| b = min(a + size, n) | |
| out.append((a, b)) | |
| if b == n: | |
| return out | |
| a += step | |
| class Restorer: | |
| def __init__(self, model_dir=DEFAULT_MODEL, device="cpu", model=None, tokenizer=None, use_lex=True, use_bias=True, quantize=False, | |
| win_words=150, win_step=100, max_tokens=512): | |
| from transformers import AutoModelForTokenClassification, AutoTokenizer | |
| self.device = device | |
| self.tok = tokenizer or AutoTokenizer.from_pretrained(model_dir) | |
| if model is None: | |
| model = AutoModelForTokenClassification.from_pretrained(model_dir) | |
| model.to(device).eval() | |
| if quantize and device == "cpu": | |
| # int8 dynamique sur les couches lineaires (option, CPU seulement). | |
| model = torch.ao.quantization.quantize_dynamic(model, {torch.nn.Linear}, dtype=torch.qint8) | |
| self.model = model | |
| tables = {"mixed": {}, "lex": {}} | |
| tpath = os.path.join(model_dir, "tables.json") if model_dir else "" | |
| if tpath and os.path.exists(tpath): | |
| tables = json.load(open(tpath)) | |
| self.mixed = tables.get("mixed", {}) | |
| self.lex = tables.get("lex", {}) if use_lex else {} | |
| self.win_words, self.win_step, self.max_tokens = win_words, win_step, max_tokens | |
| # Biais additif par signe sur log P(ponct.), regle sur la validation (tune_bias.py). | |
| self.bias = np.zeros(NP, dtype=np.float32) | |
| bpath = os.path.join(model_dir, "punct_bias.json") if model_dir else "" | |
| if use_bias and bpath and os.path.exists(bpath): | |
| b = json.load(open(bpath)) | |
| self.bias = np.array([b.get(p, 0.0) for p in PUNCTS], dtype=np.float32) | |
| def predict_words(self, batch_words, batch_size=32): | |
| """Liste de listes de mots -> liste de (casses, ponctuations). Argmax des marginales.""" | |
| out = [] | |
| for w, (pc, pp, seen) in zip(batch_words, self.predict_probs(batch_words, batch_size)): | |
| cases = [CASES[i] for i in pc.argmax(1)] if len(w) else [] | |
| puncts = [PUNCTS[i] for i in (np.log(pp + 1e-9) + self.bias).argmax(1)] if len(w) else [] | |
| # Mot jamais vu (tronque) : pas de prediction -> minuscule, sans signe. | |
| for g in range(len(w)): | |
| if not seen[g]: | |
| cases[g], puncts[g] = "L", "O" | |
| out.append((cases, puncts)) | |
| return out | |
| def predict_probs(self, batch_words, batch_size=32): | |
| """Liste de listes de mots -> [(P(casse) [n,4], P(ponct.) [n,7], vu [n])] par exemple.""" | |
| jobs = [] # (index exemple, a, b) | |
| for k, w in enumerate(batch_words): | |
| for a, b in windows(len(w), self.win_words, self.win_step): | |
| jobs.append((k, a, b)) | |
| probs = [np.zeros((len(w), len(CASES) * NP), dtype=np.float32) for w in batch_words] | |
| best = [np.full(len(w), -1.0) for w in batch_words] | |
| jobs.sort(key=lambda j: j[2] - j[1]) | |
| for s in range(0, len(jobs), batch_size): | |
| chunk = jobs[s : s + batch_size] | |
| enc = self.tok([batch_words[k][a:b] for k, a, b in chunk], is_split_into_words=True, | |
| truncation=True, max_length=self.max_tokens, padding=True, return_tensors="pt") | |
| logits = self.model(input_ids=enc["input_ids"].to(self.device), | |
| attention_mask=enc["attention_mask"].to(self.device)).logits.float() | |
| p = torch.softmax(logits, -1).cpu().numpy() | |
| for j, (k, a, b) in enumerate(chunk): | |
| prev = None | |
| for t, wid in enumerate(enc.word_ids(j)): | |
| if wid is None or wid == prev: | |
| prev = wid | |
| continue | |
| prev = wid | |
| g = a + wid | |
| # centralite : distance au bord de fenetre (les bords de texte ne comptent pas) | |
| left = wid if a > 0 else 10**6 | |
| right = (b - a - 1 - wid) if b < len(batch_words[k]) else 10**6 | |
| c = min(left, right) | |
| if c > best[k][g]: | |
| best[k][g] = c | |
| probs[k][g] = p[j, t] | |
| out = [] | |
| for k, w in enumerate(batch_words): | |
| pr = probs[k].reshape(len(w), len(CASES), NP) | |
| out.append((pr.sum(2), pr.sum(1), best[k] >= 0)) | |
| return out | |
| def restore_many(self, texts, batch_size=32): | |
| words = [normalize(t).split() for t in texts] | |
| preds = self.predict_words(words, batch_size=batch_size) | |
| outs = [] | |
| for w, (c, p) in zip(words, preds): | |
| o = render(w, c, p, self.mixed, self.lex) if w else "" | |
| assert normalize(o).split() == w, (w, o) | |
| outs.append(o) | |
| return outs | |
| def restore(self, text): | |
| return self.restore_many([text])[0] | |
| _DEFAULT = None | |
| def restore(text: str) -> str: | |
| """Restaure majuscules et ponctuation (CPU). Le modele est charge au premier appel.""" | |
| global _DEFAULT | |
| if _DEFAULT is None: | |
| _DEFAULT = Restorer() | |
| return _DEFAULT.restore(text) | |
| def bench(n, threads, model_dir, quantize=False): | |
| """Latence CPU, une phrase a la fois, sur n hypotheses ASR de la VALIDATION.""" | |
| torch.set_num_threads(threads) | |
| r = Restorer(model_dir, quantize=quantize) | |
| path = os.path.join(HERE, "asr_valid", "valid_lm_transcripts.jsonl") | |
| if os.path.exists(path): | |
| texts = [json.loads(l)["hyp"] for l in open(path)] | |
| import random | |
| texts = random.Random(0).sample(texts, min(n, len(texts))) # le fichier est trie par duree | |
| else: | |
| texts = [" ".join(json.loads(l)["words"]) for l in open(os.path.join(HERE, "data", "valid.jsonl"))][:n] | |
| for t in texts[:5]: | |
| r.restore(t) | |
| lat = [] | |
| for t in texts: | |
| t0 = time.perf_counter() | |
| r.restore(t) | |
| lat.append(1000 * (time.perf_counter() - t0)) | |
| lat = np.array(lat) | |
| words = np.mean([len(t.split()) for t in texts]) | |
| res = {"n": len(texts), "threads": threads, "int8": quantize, "mots_moyens": round(float(words), 1), | |
| "median_ms": round(float(np.median(lat)), 1), "p90_ms": round(float(np.percentile(lat, 90)), 1), | |
| "max_ms": round(float(lat.max()), 1), "moyenne_ms": round(float(lat.mean()), 1)} | |
| print(json.dumps(res, ensure_ascii=False)) | |
| return res | |
| if __name__ == "__main__": | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("text", nargs="?") | |
| ap.add_argument("--model", default=DEFAULT_MODEL) | |
| ap.add_argument("--bench", type=int, default=0) | |
| ap.add_argument("--threads", type=int, default=2) | |
| ap.add_argument("--out", default=None) | |
| ap.add_argument("--int8", action="store_true", help="quantification dynamique int8 (CPU)") | |
| a = ap.parse_args() | |
| if a.bench: | |
| res = bench(a.bench, a.threads, a.model, a.int8) | |
| if a.out: | |
| json.dump(res, open(a.out, "w"), ensure_ascii=False, indent=2) | |
| else: | |
| torch.set_num_threads(a.threads) | |
| print(Restorer(a.model, quantize=a.int8).restore(a.text or sys.stdin.read())) | |