Flo976 commited on
Commit
7df18eb
·
verified ·
1 Parent(s): ce3b8d8

Premiere publication

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - mg
4
+ license: cc-by-sa-4.0
5
+ library_name: transformers
6
+ pipeline_tag: token-classification
7
+ base_model: Davlan/xlm-roberta-base-finetuned-malagasy
8
+ datasets:
9
+ - badrex/malagasy-speech-full
10
+ tags:
11
+ - punctuation-restoration
12
+ - truecasing
13
+ - malagasy
14
+ - asr-postprocessing
15
+ - xlm-roberta
16
+ ---
17
+
18
+ <p align="center"><a href="#results"><img src="https://img.shields.io/badge/formatted%20WER%20on%20ASR%20output-18.5%20%25%20vs%2021.7%20%25-2F7D55?style=flat-square&labelColor=1E2A5E" alt="formatted WER on ASR output: 18.5 % against 21.7 %"></a> <a href="#results"><img src="https://img.shields.io/badge/period%20F1-79-2F7D55?style=flat-square&labelColor=1E2A5E" alt="period F1: 79"></a> <a href="#what-it-does"><img src="https://img.shields.io/badge/words-never%20changed-E9C46A?style=flat-square&labelColor=1E2A5E" alt="words: never changed"></a> <a href="#license"><img src="https://img.shields.io/badge/license-CC--BY--SA--4.0-8A93A6?style=flat-square&labelColor=1E2A5E" alt="license: CC-BY-SA-4.0"></a> <a href="https://huggingface.co/spaces/Flo976/malagasy-asr-demo"><img src="https://img.shields.io/badge/demo-try%20it%20in%20the%20browser-E9C46A?style=flat-square&labelColor=1E2A5E" alt="demo: try it in the browser"></a></p>
19
+
20
+ # Malagasy casing and punctuation restoration for speech recognition
21
+
22
+ Speech recognition models such as [Flo976/w2v-bert-malagasy](https://huggingface.co/Flo976/w2v-bert-malagasy) output lowercase text without punctuation. This model puts back **capital letters, commas, periods, question and exclamation marks and colons, without ever changing a word**, and restores accented letters that ASR normalization strips (`tan na` → `tanàna`).
23
+
24
+ ```
25
+ in: vehivavy fohy volo iray ary miloko mainty manamboatra volo manoloana ny fitaratra
26
+ out: Vehivavy fohy volo iray ary miloko mainty, manamboatra volo manoloana ny fitaratra.
27
+ ```
28
+
29
+ **Résumé en français.** Remet les majuscules et la ponctuation dans la sortie d'un modèle de reconnaissance vocale malgache, sans jamais changer les mots. Sur la vraie sortie de l'ASR, le texte final mis en forme compte 18,5 % d'erreurs contre 21,7 % pour une mise en forme minimale (majuscule initiale et point final). Les points sont bien placés (F1 79), les virgules une fois sur deux environ (F1 52).
30
+
31
+ ## What it does
32
+
33
+ For every word it predicts a case (lower, Capitalized, UPPER, mixed) and the mark that follows it: none, `,` `.` `?` `!` `:` or `;`. Since it classifies words instead of generating text, it cannot invent, drop or rewrite words: every call asserts that `normalize(output) == normalize(input)`, where `normalize` is the lowercase, punctuation-free form used to score the ASR. This assertion held on every test output and in a fuzz test.
34
+
35
+ ## Results
36
+
37
+ Test split of [badrex/malagasy-speech-full](https://huggingface.co/datasets/badrex/malagasy-speech-full), 3,101 clips, speakers never seen in training. *Formatted WER* compares the final text to the original transcript **with** its casing and punctuation, without any normalization: it measures what a reader actually sees.
38
+
39
+ | Input | System | Formatted WER | F1 macro `, . ? ! :` | F1 comma | F1 period | Case accuracy |
40
+ | --- | --- | --- | --- | --- | --- | --- |
41
+ | real ASR output (10.6 % WER) | capitalize first word + final period | 21.73 % | 13.1 | 0 | 65.6 | 96.0 % |
42
+ | real ASR output (10.6 % WER) | **this model** | **18.47 %** | **45.2** | 51.9 | 79.0 | 97.1 % |
43
+ | reference words | capitalize first word + final period | 13.09 % | 13.3 | 0 | 66.3 | 96.1 % |
44
+ | reference words | **this model** | **9.18 %** | **51.2** | 52.7 | 79.8 | 97.3 % |
45
+
46
+ On real ASR output the gain over the trivial baseline is **−3.26 points** of formatted WER (paired bootstrap, 95 % CI [−3.51, −3.02]). With perfect labels, the floor would be 2.4 %: quotes, brackets and some accents are out of scope.
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ import os, sys
52
+ from huggingface_hub import snapshot_download
53
+
54
+ path = snapshot_download("Flo976/malagasy-punctuation")
55
+ sys.path.insert(0, path)
56
+ from punctuate import Restorer
57
+
58
+ restorer = Restorer(path) # CPU by default; Restorer(path, device="cuda") on GPU
59
+ print(restorer.restore("tena mamy tokoa ity voankazo ity ary tsy mifidy"))
60
+ ```
61
+
62
+ Requirements: `torch`, `transformers`, `sentencepiece`, `numpy`. Long texts are processed in sliding windows of 150 words. On two CPU threads, a 32-word utterance takes about 80 ms.
63
+
64
+ The [demo](https://huggingface.co/spaces/Flo976/malagasy-asr-demo) also capitalizes the word after `.`, `?` and `!`, as 99.6 % of sentence starts are in the validation transcripts. That rule is not included in the measurements above.
65
+
66
+ ## Training
67
+
68
+ | | |
69
+ | --- | --- |
70
+ | Base model | [Davlan/xlm-roberta-base-finetuned-malagasy](https://huggingface.co/Davlan/xlm-roberta-base-finetuned-malagasy), XLM-R base adapted to Malagasy |
71
+ | Head | one token-classification head, 28 classes (4 cases × 7 marks), read on the first sub-token of each word |
72
+ | Data | 0.93 M words of raw train-split transcripts of badrex/malagasy-speech-full + 1.5 M words of filtered Malagasy Wikipedia sentences |
73
+ | Robustness | half of the training inputs receive synthetic CTC-style errors (substituted letters, merged or split words) |
74
+ | Accent table | built from the training text, restores letters that normalization strips; same no-word-change guarantee |
75
+ | Selection | on the validation split only; the test was used once |
76
+ | Hardware | one RTX 5070 Ti, 13 minutes |
77
+
78
+ ## Limitations
79
+
80
+ - **Commas remain hard** (F1 about 52): transcribers place them inconsistently.
81
+ - **Question and exclamation marks are rare** in the test (20 and 35 cases): their scores are very uncertain. Semicolons are never predicted.
82
+ - **Domain**: trained on read image descriptions and Wikipedia. Dialogue and questions, as in a voice assistant, are less well punctuated.
83
+ - **Size**: 1.1 GB in fp32. Dynamic int8 quantization breaks it (comma F1 from 54 to 19): do not quantize.
84
+ - Pauses in the audio are not used. Adding them helped a separate classifier, but they are not integrated in this model.
85
+
86
+ ## License
87
+
88
+ **CC-BY-SA-4.0**, because the model is trained on Malagasy Wikipedia (CC BY-SA). The base model, Davlan/xlm-roberta-base-finetuned-malagasy, declares no license on its card; XLM-R itself is MIT. The speech dataset, badrex/malagasy-speech-full, declares no license either. Please credit Davlan (David Adelani) for the base model.
89
+
90
+ Made for [Milo Voice](https://github.com/Flo976/milo) by [Flo976](https://huggingface.co/Flo976) — Florent Didelot / [Sooatek](https://sooatek.com).
common.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Briques communes du chantier ponctuation/majuscules.
2
+
3
+ Schema d'etiquettes, par mot NORMALISE (normalize() d'evaluate_asr.py) :
4
+ - casse : L (minuscule), C (Capitale initiale), U (TOUT EN MAJUSCULES, >= 2 lettres),
5
+ M (mixte, ex. "McDonald", "iPhone" : forme rendue via une table apprise)
6
+ - ponct. : signe qui suit le mot : O (aucun) , . ? ! : ;
7
+ ("..." est compte comme "." ; guillemets, parentheses, tirets
8
+ d'incise ne sont pas predits : voir README).
9
+
10
+ Alignement texte brut -> mots normalises : normalize() agit caractere par
11
+ caractere a l'interieur de chaque jeton separe par des blancs (minuscules,
12
+ puis tout caractere hors [a-z0-9'-] devient un blanc, puis les jetons "-" et
13
+ "'" isoles sont supprimes). On rejoue exactement cette operation en gardant la
14
+ position de chaque caractere dans le texte brut : chaque mot normalise est
15
+ alors une plage du texte brut, d'ou on lit la casse ; le "trou" entre deux mots
16
+ (blancs, ponctuation, lettres accentuees supprimees) donne la ponctuation.
17
+ Si la reconstruction ne redonne pas normalize(texte).split(), l'exemple est
18
+ ignore et compte.
19
+ """
20
+ import os
21
+ import re
22
+ import sys
23
+ import unicodedata
24
+
25
+ try:
26
+ from evaluate_asr import APOS, DASH, normalize # noqa: E402
27
+ except ImportError: # hors du depot (Space, paquet du modele) : copie conforme
28
+ APOS = dict.fromkeys(map(ord, "’ʼʻ`´"), "'")
29
+ DASH = dict.fromkeys(map(ord, "‐‑‒–—―"), "-")
30
+ _KEEP_RE = re.compile(r"[^a-z0-9'\- ]+")
31
+
32
+ def normalize(text):
33
+ if not text:
34
+ return ""
35
+ text = unicodedata.normalize("NFC", text).translate(APOS).translate(DASH).lower()
36
+ text = re.sub(r"\s+", " ", _KEEP_RE.sub(" ", text)).strip()
37
+ return " ".join(w for w in text.split() if w not in {"-", "'"})
38
+
39
+ CASES = ["L", "C", "U", "M"]
40
+ PUNCTS = ["O", ",", ".", "?", "!", ":", ";"]
41
+ CASE2ID = {c: i for i, c in enumerate(CASES)}
42
+ P2ID = {p: i for i, p in enumerate(PUNCTS)}
43
+ ALLOWED = set("abcdefghijklmnopqrstuvwxyz0123456789'- ")
44
+ # Priorite quand un trou contient plusieurs signes (ex. '".' ou ').').
45
+ PRIORITY = ["?", "!", ".", ":", ";", ","]
46
+
47
+
48
+ def gap_punct(gap):
49
+ for p in PRIORITY:
50
+ if p in gap:
51
+ return p
52
+ return "O"
53
+
54
+
55
+ def case_of(s):
56
+ letters = [c for c in s if c.isalpha()]
57
+ if not letters:
58
+ return "L"
59
+ if all(c.islower() for c in letters):
60
+ return "L"
61
+ if letters[0].isupper() and all(c.islower() for c in letters[1:]):
62
+ return "C"
63
+ if len(letters) >= 2 and all(c.isupper() for c in letters):
64
+ return "U"
65
+ if len(letters) == 1:
66
+ return "C"
67
+ return "M"
68
+
69
+
70
+ def segments(raw):
71
+ """Rend [(debut, fin)] des mots normalises dans le texte brut (NFC), et le texte NFC.
72
+
73
+ Les positions renvoient au texte NFC + apostrophes/tirets unifies (meme
74
+ longueur que le NFC, la traduction etant caractere pour caractere)."""
75
+ t = unicodedata.normalize("NFC", raw).translate(APOS).translate(DASH)
76
+ segs, start = [], None
77
+ for i, ch in enumerate(t):
78
+ low = ch.lower()
79
+ keep = len(low) == 1 and low in ALLOWED and low != " "
80
+ if ch.isspace():
81
+ keep = False
82
+ if keep:
83
+ if start is None:
84
+ start = i
85
+ else:
86
+ if start is not None:
87
+ segs.append((start, i))
88
+ start = None
89
+ if start is not None:
90
+ segs.append((start, len(t)))
91
+ segs = [(a, b) for a, b in segs if t[a:b] not in ("-", "'")]
92
+ return segs, t
93
+
94
+
95
+ def label_text(raw):
96
+ """Texte brut -> (mots normalises, casses, ponctuations, formes brutes) ou None si desaccord."""
97
+ segs, t = segments(raw)
98
+ words = [t[a:b].lower() for a, b in segs]
99
+ if words != normalize(raw).split():
100
+ return None
101
+ cases, puncts, forms = [], [], []
102
+ for k, (a, b) in enumerate(segs):
103
+ nxt = segs[k + 1][0] if k + 1 < len(segs) else len(t)
104
+ cases.append(case_of(t[a:b]))
105
+ puncts.append(gap_punct(t[b:nxt]))
106
+ forms.append(t[a:b])
107
+ return words, cases, puncts, forms
108
+
109
+
110
+ def apply_case(word, c, mixed_table=None):
111
+ if c == "L":
112
+ return word
113
+ if c == "U":
114
+ return word.upper()
115
+ if c == "M" and mixed_table and word in mixed_table:
116
+ return mixed_table[word]
117
+ start = 0
118
+ if c == "M":
119
+ # M hors table : le cas typique est un nom propre apres elision ou
120
+ # trait d'union ("an'Antananarivo", "maha-Malagasy").
121
+ cut = max(word.rfind("'"), word.rfind("-"))
122
+ if 0 <= cut < len(word) - 1:
123
+ start = cut + 1
124
+ # C : premiere lettre en majuscule.
125
+ for i, ch in enumerate(word):
126
+ if i < start:
127
+ continue
128
+ if ch.isalpha():
129
+ return word[:i] + ch.upper() + word[i + 1 :]
130
+ return word
131
+
132
+
133
+ def render(words, cases, puncts, mixed_table=None, lex_table=None):
134
+ """Assemble la sortie. lex_table (optionnelle) : n-gramme normalise -> forme brute
135
+ minuscule accentuee (ex. ("tan","na") -> "tanàna"), appliquee avant la casse.
136
+ La sortie verifie toujours normalize(sortie).split() == words."""
137
+ out, i, n = [], 0, len(words)
138
+ while i < n:
139
+ span = 1
140
+ form = None
141
+ if lex_table:
142
+ for k in (3, 2, 1):
143
+ if i + k <= n:
144
+ key = " ".join(words[i : i + k])
145
+ if key in lex_table:
146
+ form, span = lex_table[key], k
147
+ break
148
+ if form is None:
149
+ tok = apply_case(words[i], cases[i], mixed_table)
150
+ else:
151
+ tok = apply_case(form, cases[i], None)
152
+ p = puncts[i + span - 1]
153
+ out.append(tok + ("" if p == "O" else p))
154
+ i += span
155
+ return " ".join(out)
156
+
157
+
158
+ def check_words(inp_words, text):
159
+ return normalize(text).split() == inp_words
160
+
161
+
162
+ def trivial_baseline(norm_text):
163
+ """Baseline triviale : majuscule en debut + point final."""
164
+ w = norm_text.split()
165
+ if not w:
166
+ return ""
167
+ return render(w, ["C"] + ["L"] * (len(w) - 1), ["O"] * (len(w) - 1) + ["."])
168
+
169
+
170
+ WORK = os.path.dirname(os.path.abspath(__file__))
config.json ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "XLMRobertaForTokenClassification"
4
+ ],
5
+ "attention_probs_dropout_prob": 0.1,
6
+ "bos_token_id": 0,
7
+ "classifier_dropout": null,
8
+ "dtype": "float32",
9
+ "eos_token_id": 2,
10
+ "hidden_act": "gelu",
11
+ "hidden_dropout_prob": 0.1,
12
+ "hidden_size": 768,
13
+ "id2label": {
14
+ "0": "L|O",
15
+ "1": "L|,",
16
+ "2": "L|.",
17
+ "3": "L|?",
18
+ "4": "L|!",
19
+ "5": "L|:",
20
+ "6": "L|;",
21
+ "7": "C|O",
22
+ "8": "C|,",
23
+ "9": "C|.",
24
+ "10": "C|?",
25
+ "11": "C|!",
26
+ "12": "C|:",
27
+ "13": "C|;",
28
+ "14": "U|O",
29
+ "15": "U|,",
30
+ "16": "U|.",
31
+ "17": "U|?",
32
+ "18": "U|!",
33
+ "19": "U|:",
34
+ "20": "U|;",
35
+ "21": "M|O",
36
+ "22": "M|,",
37
+ "23": "M|.",
38
+ "24": "M|?",
39
+ "25": "M|!",
40
+ "26": "M|:",
41
+ "27": "M|;"
42
+ },
43
+ "initializer_range": 0.02,
44
+ "intermediate_size": 3072,
45
+ "label2id": {
46
+ "C|!": 11,
47
+ "C|,": 8,
48
+ "C|.": 9,
49
+ "C|:": 12,
50
+ "C|;": 13,
51
+ "C|?": 10,
52
+ "C|O": 7,
53
+ "L|!": 4,
54
+ "L|,": 1,
55
+ "L|.": 2,
56
+ "L|:": 5,
57
+ "L|;": 6,
58
+ "L|?": 3,
59
+ "L|O": 0,
60
+ "M|!": 25,
61
+ "M|,": 22,
62
+ "M|.": 23,
63
+ "M|:": 26,
64
+ "M|;": 27,
65
+ "M|?": 24,
66
+ "M|O": 21,
67
+ "U|!": 18,
68
+ "U|,": 15,
69
+ "U|.": 16,
70
+ "U|:": 19,
71
+ "U|;": 20,
72
+ "U|?": 17,
73
+ "U|O": 14
74
+ },
75
+ "layer_norm_eps": 1e-05,
76
+ "max_position_embeddings": 514,
77
+ "model_type": "xlm-roberta",
78
+ "num_attention_heads": 12,
79
+ "num_hidden_layers": 12,
80
+ "output_past": true,
81
+ "pad_token_id": 1,
82
+ "position_embedding_type": "absolute",
83
+ "transformers_version": "4.57.1",
84
+ "type_vocab_size": 1,
85
+ "use_cache": true,
86
+ "vocab_size": 250002
87
+ }
labels.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema": "etiquette combinee = indice_casse * 7 + indice_ponct, lue sur le premier sous-jeton de chaque mot normalise",
3
+ "cases": {
4
+ "L": "minuscule",
5
+ "C": "Capitale initiale",
6
+ "U": "TOUT EN MAJUSCULES",
7
+ "M": "mixte (forme lue dans tables.json['mixed'], sinon capitale apres la derniere apostrophe/trait d'union)"
8
+ },
9
+ "puncts": [
10
+ "O",
11
+ ",",
12
+ ".",
13
+ "?",
14
+ "!",
15
+ ":",
16
+ ";"
17
+ ],
18
+ "normalisation_entree": "evaluate_asr.normalize (minuscules, [a-z0-9'-], apostrophes/tirets unifies)",
19
+ "tables": "tables.json : 'mixed' = formes a casse mixte ; 'lex' = n-gramme normalise -> forme accentuee (ex. 'tan na' -> 'tanàna'), construite sur train + Wikipedia",
20
+ "base": "Davlan/xlm-roberta-base-finetuned-malagasy",
21
+ "entrainement": "work/punct/train.py --noise 0.5 --epochs 3 (checkpoint epoque 2.5)"
22
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13887a16c47f554d329ccfa6e88c081db0a1139bfa2b5c7096a79d57165eb415
3
+ size 1109922400
punctuate.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Restauration des majuscules et de la ponctuation pour la sortie ASR malgache.
3
+
4
+ from punctuate import restore
5
+ restore("lanonana ara-panambadiana iray ahitana karazan'olona mandihy")
6
+ -> "Lanonana ara-panambadiana iray ahitana karazan'olona mandihy."
7
+
8
+ Garantie : normalize(sortie).split() == normalize(entree).split() (assert).
9
+ Les mots ne sont jamais changes ; seules la casse et la ponctuation qui suit
10
+ chaque mot sont predites (plus, en option, la remise des lettres accentuees
11
+ qu'efface normalize(), ex. "tan na" -> "tanàna", qui respecte la meme garantie).
12
+
13
+ Modele : XLM-R base (Davlan/xlm-roberta-base-finetuned-malagasy) + une tete de
14
+ classification de jetons combinee (4 casses x 7 ponctuations), etiquette lue
15
+ sur le premier sous-jeton de chaque mot. Textes longs : fenetres glissantes.
16
+
17
+ CLI :
18
+ python punctuate.py "texte en minuscules" # restaure
19
+ python punctuate.py --bench 100 --threads 2 # latence CPU
20
+ """
21
+ import argparse
22
+ import json
23
+ import os
24
+ import sys
25
+ import time
26
+
27
+ import numpy as np
28
+ import torch
29
+
30
+ HERE = os.path.dirname(os.path.abspath(__file__))
31
+ sys.path.insert(0, HERE)
32
+ from common import CASES, PUNCTS, normalize, render # noqa: E402
33
+
34
+ DEFAULT_MODEL = os.environ.get("MG_PUNCT_DIR", HERE)
35
+ NP = len(PUNCTS)
36
+
37
+
38
+ def windows(n, size, step):
39
+ """Fenetres [a, b) couvrant n mots ; chaque mot sera lu dans la fenetre ou il est le plus central."""
40
+ if n <= size:
41
+ return [(0, n)]
42
+ out, a = [], 0
43
+ while True:
44
+ b = min(a + size, n)
45
+ out.append((a, b))
46
+ if b == n:
47
+ return out
48
+ a += step
49
+
50
+
51
+ class Restorer:
52
+ def __init__(self, model_dir=DEFAULT_MODEL, device="cpu", model=None, tokenizer=None, use_lex=True, use_bias=True, quantize=False,
53
+ win_words=150, win_step=100, max_tokens=512):
54
+ from transformers import AutoModelForTokenClassification, AutoTokenizer
55
+
56
+ self.device = device
57
+ self.tok = tokenizer or AutoTokenizer.from_pretrained(model_dir)
58
+ if model is None:
59
+ model = AutoModelForTokenClassification.from_pretrained(model_dir)
60
+ model.to(device).eval()
61
+ if quantize and device == "cpu":
62
+ # int8 dynamique sur les couches lineaires (option, CPU seulement).
63
+ model = torch.ao.quantization.quantize_dynamic(model, {torch.nn.Linear}, dtype=torch.qint8)
64
+ self.model = model
65
+ tables = {"mixed": {}, "lex": {}}
66
+ tpath = os.path.join(model_dir, "tables.json") if model_dir else ""
67
+ if tpath and os.path.exists(tpath):
68
+ tables = json.load(open(tpath))
69
+ self.mixed = tables.get("mixed", {})
70
+ self.lex = tables.get("lex", {}) if use_lex else {}
71
+ self.win_words, self.win_step, self.max_tokens = win_words, win_step, max_tokens
72
+ # Biais additif par signe sur log P(ponct.), regle sur la validation (tune_bias.py).
73
+ self.bias = np.zeros(NP, dtype=np.float32)
74
+ bpath = os.path.join(model_dir, "punct_bias.json") if model_dir else ""
75
+ if use_bias and bpath and os.path.exists(bpath):
76
+ b = json.load(open(bpath))
77
+ self.bias = np.array([b.get(p, 0.0) for p in PUNCTS], dtype=np.float32)
78
+
79
+ def predict_words(self, batch_words, batch_size=32):
80
+ """Liste de listes de mots -> liste de (casses, ponctuations). Argmax des marginales."""
81
+ out = []
82
+ for w, (pc, pp, seen) in zip(batch_words, self.predict_probs(batch_words, batch_size)):
83
+ cases = [CASES[i] for i in pc.argmax(1)] if len(w) else []
84
+ puncts = [PUNCTS[i] for i in (np.log(pp + 1e-9) + self.bias).argmax(1)] if len(w) else []
85
+ # Mot jamais vu (tronque) : pas de prediction -> minuscule, sans signe.
86
+ for g in range(len(w)):
87
+ if not seen[g]:
88
+ cases[g], puncts[g] = "L", "O"
89
+ out.append((cases, puncts))
90
+ return out
91
+
92
+ @torch.no_grad()
93
+ def predict_probs(self, batch_words, batch_size=32):
94
+ """Liste de listes de mots -> [(P(casse) [n,4], P(ponct.) [n,7], vu [n])] par exemple."""
95
+ jobs = [] # (index exemple, a, b)
96
+ for k, w in enumerate(batch_words):
97
+ for a, b in windows(len(w), self.win_words, self.win_step):
98
+ jobs.append((k, a, b))
99
+ probs = [np.zeros((len(w), len(CASES) * NP), dtype=np.float32) for w in batch_words]
100
+ best = [np.full(len(w), -1.0) for w in batch_words]
101
+ jobs.sort(key=lambda j: j[2] - j[1])
102
+ for s in range(0, len(jobs), batch_size):
103
+ chunk = jobs[s : s + batch_size]
104
+ enc = self.tok([batch_words[k][a:b] for k, a, b in chunk], is_split_into_words=True,
105
+ truncation=True, max_length=self.max_tokens, padding=True, return_tensors="pt")
106
+ logits = self.model(input_ids=enc["input_ids"].to(self.device),
107
+ attention_mask=enc["attention_mask"].to(self.device)).logits.float()
108
+ p = torch.softmax(logits, -1).cpu().numpy()
109
+ for j, (k, a, b) in enumerate(chunk):
110
+ prev = None
111
+ for t, wid in enumerate(enc.word_ids(j)):
112
+ if wid is None or wid == prev:
113
+ prev = wid
114
+ continue
115
+ prev = wid
116
+ g = a + wid
117
+ # centralite : distance au bord de fenetre (les bords de texte ne comptent pas)
118
+ left = wid if a > 0 else 10**6
119
+ right = (b - a - 1 - wid) if b < len(batch_words[k]) else 10**6
120
+ c = min(left, right)
121
+ if c > best[k][g]:
122
+ best[k][g] = c
123
+ probs[k][g] = p[j, t]
124
+ out = []
125
+ for k, w in enumerate(batch_words):
126
+ pr = probs[k].reshape(len(w), len(CASES), NP)
127
+ out.append((pr.sum(2), pr.sum(1), best[k] >= 0))
128
+ return out
129
+
130
+ def restore_many(self, texts, batch_size=32):
131
+ words = [normalize(t).split() for t in texts]
132
+ preds = self.predict_words(words, batch_size=batch_size)
133
+ outs = []
134
+ for w, (c, p) in zip(words, preds):
135
+ o = render(w, c, p, self.mixed, self.lex) if w else ""
136
+ assert normalize(o).split() == w, (w, o)
137
+ outs.append(o)
138
+ return outs
139
+
140
+ def restore(self, text):
141
+ return self.restore_many([text])[0]
142
+
143
+
144
+ _DEFAULT = None
145
+
146
+
147
+ def restore(text: str) -> str:
148
+ """Restaure majuscules et ponctuation (CPU). Le modele est charge au premier appel."""
149
+ global _DEFAULT
150
+ if _DEFAULT is None:
151
+ _DEFAULT = Restorer()
152
+ return _DEFAULT.restore(text)
153
+
154
+
155
+ def bench(n, threads, model_dir, quantize=False):
156
+ """Latence CPU, une phrase a la fois, sur n hypotheses ASR de la VALIDATION."""
157
+ torch.set_num_threads(threads)
158
+ r = Restorer(model_dir, quantize=quantize)
159
+ path = os.path.join(HERE, "asr_valid", "valid_lm_transcripts.jsonl")
160
+ if os.path.exists(path):
161
+ texts = [json.loads(l)["hyp"] for l in open(path)]
162
+ import random
163
+ texts = random.Random(0).sample(texts, min(n, len(texts))) # le fichier est trie par duree
164
+ else:
165
+ texts = [" ".join(json.loads(l)["words"]) for l in open(os.path.join(HERE, "data", "valid.jsonl"))][:n]
166
+ for t in texts[:5]:
167
+ r.restore(t)
168
+ lat = []
169
+ for t in texts:
170
+ t0 = time.perf_counter()
171
+ r.restore(t)
172
+ lat.append(1000 * (time.perf_counter() - t0))
173
+ lat = np.array(lat)
174
+ words = np.mean([len(t.split()) for t in texts])
175
+ res = {"n": len(texts), "threads": threads, "int8": quantize, "mots_moyens": round(float(words), 1),
176
+ "median_ms": round(float(np.median(lat)), 1), "p90_ms": round(float(np.percentile(lat, 90)), 1),
177
+ "max_ms": round(float(lat.max()), 1), "moyenne_ms": round(float(lat.mean()), 1)}
178
+ print(json.dumps(res, ensure_ascii=False))
179
+ return res
180
+
181
+
182
+ if __name__ == "__main__":
183
+ ap = argparse.ArgumentParser()
184
+ ap.add_argument("text", nargs="?")
185
+ ap.add_argument("--model", default=DEFAULT_MODEL)
186
+ ap.add_argument("--bench", type=int, default=0)
187
+ ap.add_argument("--threads", type=int, default=2)
188
+ ap.add_argument("--out", default=None)
189
+ ap.add_argument("--int8", action="store_true", help="quantification dynamique int8 (CPU)")
190
+ a = ap.parse_args()
191
+ if a.bench:
192
+ res = bench(a.bench, a.threads, a.model, a.int8)
193
+ if a.out:
194
+ json.dump(res, open(a.out, "w"), ensure_ascii=False, indent=2)
195
+ else:
196
+ torch.set_num_threads(a.threads)
197
+ print(Restorer(a.model, quantize=a.int8).restore(a.text or sys.stdin.read()))
sentencepiece.bpe.model ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cfc8146abe2a0488e9e2a0c56de7952f7c11ab059eca145a0a727afce0db2865
3
+ size 5069051
special_tokens_map.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": {
3
+ "content": "<s>",
4
+ "lstrip": false,
5
+ "normalized": false,
6
+ "rstrip": false,
7
+ "single_word": false
8
+ },
9
+ "cls_token": {
10
+ "content": "<s>",
11
+ "lstrip": false,
12
+ "normalized": false,
13
+ "rstrip": false,
14
+ "single_word": false
15
+ },
16
+ "eos_token": {
17
+ "content": "</s>",
18
+ "lstrip": false,
19
+ "normalized": false,
20
+ "rstrip": false,
21
+ "single_word": false
22
+ },
23
+ "mask_token": {
24
+ "content": "<mask>",
25
+ "lstrip": true,
26
+ "normalized": false,
27
+ "rstrip": false,
28
+ "single_word": false
29
+ },
30
+ "pad_token": {
31
+ "content": "<pad>",
32
+ "lstrip": false,
33
+ "normalized": false,
34
+ "rstrip": false,
35
+ "single_word": false
36
+ },
37
+ "sep_token": {
38
+ "content": "</s>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false
43
+ },
44
+ "unk_token": {
45
+ "content": "<unk>",
46
+ "lstrip": false,
47
+ "normalized": false,
48
+ "rstrip": false,
49
+ "single_word": false
50
+ }
51
+ }
tables.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:883b037111086fd4dfebbbc9b7cee11e1517b5e0c0514879478661440f137085
3
+ size 17082987
tokenizer_config.json ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "<s>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "1": {
12
+ "content": "<pad>",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "2": {
20
+ "content": "</s>",
21
+ "lstrip": false,
22
+ "normalized": false,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "3": {
28
+ "content": "<unk>",
29
+ "lstrip": false,
30
+ "normalized": false,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": true
34
+ },
35
+ "250001": {
36
+ "content": "<mask>",
37
+ "lstrip": true,
38
+ "normalized": false,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": true
42
+ }
43
+ },
44
+ "bos_token": "<s>",
45
+ "clean_up_tokenization_spaces": false,
46
+ "cls_token": "<s>",
47
+ "eos_token": "</s>",
48
+ "extra_special_tokens": {},
49
+ "mask_token": "<mask>",
50
+ "model_max_length": 512,
51
+ "pad_token": "<pad>",
52
+ "sep_token": "</s>",
53
+ "tokenizer_class": "XLMRobertaTokenizer",
54
+ "unk_token": "<unk>"
55
+ }
valid_metrics.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "step": 8070,
3
+ "epoch": 2.5,
4
+ "min": 10.9,
5
+ "valid": {
6
+ "par_signe": {
7
+ ",": {
8
+ "P": 60.66,
9
+ "R": 49.17,
10
+ "F1": 54.31,
11
+ "n_ref": 4444
12
+ },
13
+ ".": {
14
+ "P": 75.29,
15
+ "R": 84.74,
16
+ "F1": 79.73,
17
+ "n_ref": 5885
18
+ },
19
+ "?": {
20
+ "P": 52.17,
21
+ "R": 42.86,
22
+ "F1": 47.06,
23
+ "n_ref": 28
24
+ },
25
+ "!": {
26
+ "P": 76.92,
27
+ "R": 47.62,
28
+ "F1": 58.82,
29
+ "n_ref": 42
30
+ },
31
+ ":": {
32
+ "P": 34.34,
33
+ "R": 24.27,
34
+ "F1": 28.44,
35
+ "n_ref": 375
36
+ },
37
+ ";": {
38
+ "P": 0.0,
39
+ "R": 0.0,
40
+ "F1": 0.0,
41
+ "n_ref": 581
42
+ }
43
+ },
44
+ "f1_macro_5": 53.67,
45
+ "f1_micro_tous_signes": 66.63,
46
+ "casse_exactitude": 97.24,
47
+ "majuscule_F1": 80.08
48
+ },
49
+ "score": 63.394
50
+ }