@mostajs/prononciation 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +5 -2
- package/llms.txt +11 -0
- package/package.json +10 -4
- package/service/README.md +81 -0
- package/service/audio.py +38 -0
- package/service/erreurs.py +19 -0
- package/service/installer.sh +34 -0
- package/service/moteur.py +74 -0
- package/service/requirements.txt +10 -0
- package/service/serveur.py +130 -0
- package/service/sons.py +64 -0
- package/service/traits.py +62 -0
- package/src/lib/lexique.js +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
4
|
Format : Keep a Changelog ; versions : SemVer.
|
|
5
5
|
|
|
6
|
+
## [0.2.1] — 2026-10-26
|
|
7
|
+
|
|
8
|
+
### Corrigé
|
|
9
|
+
- `service/installer.sh` accepte **`PRONONCIATION_VENV`**. L'environnement Python peut vivre hors de `node_modules/`, que `npm ci` efface à chaque déploiement.
|
|
10
|
+
- `lexiqueCmudict` ignore le **commentaire de fin de ligne**. Le CMUdict de référence (cmusphinx/cmudict, `cmudict.dict`) en porte sur 22 lignes (« aalborg AO1 L B AO0 R G # place, danish »). Les mots du commentaire devenaient des sons. Essai T-PRO-24.
|
|
11
|
+
|
|
12
|
+
## [0.2.0] — 2026-10-25
|
|
13
|
+
|
|
14
|
+
### Ajouté
|
|
15
|
+
- **Le service de reconnaissance** (`service/`, Python ≥ 3.10), livré avec le module :
|
|
16
|
+
- `POST /reconnaitre` sert le contrat de `reconnaisseurHttp` ; `GET /sante` le décrit ;
|
|
17
|
+
- wav2vec2 phonémique à révision figée, et dépendances figées (`requirements.txt`) ;
|
|
18
|
+
- alignement forcé CTC et 8 traits par son attendu, repris d'`extraire_gop.py`.
|
|
19
|
+
- **Refus nommés** : ATTENDUS, AUDIO_ABSENT, LONGUEUR, TROP_GROS, TROP_LONG, FORMAT, CHEMIN, METHODE, INTERNE.
|
|
20
|
+
- **Fonctionnement** : écoute locale par défaut, une inférence à la fois. L'audio n'est jamais écrit, et le journal ne contient que des comptes et des durées.
|
|
21
|
+
- **`service/installer.sh`** : rejouable, il vérifie que le modèle se charge sans réseau.
|
|
22
|
+
- **Essais** :
|
|
23
|
+
- T-SRV-1 à 9 (unittest). Parité avec l'apprentissage sur cinq phrases : écart de traits **nul**.
|
|
24
|
+
- T-PRO-23 : le service réel derrière `reconnaisseurHttp` rend la note de Python.
|
|
25
|
+
|
|
26
|
+
### Corrigé
|
|
27
|
+
- Quand l'audio a moins de trames que de sons attendus, le partage égal débordait de l'audio, et un trait se calculait sur un segment vide. Les segments sont désormais ramenés sur la dernière trame. Les cas du corpus ne changent pas : la parité le vérifie.
|
|
28
|
+
|
|
29
|
+
### Points ouverts
|
|
30
|
+
- Le WebM/Opus enregistré par les navigateurs n'est pas lu. Il faut l'enregistreur WAV de `@mostajs/media` (B-03).
|
|
31
|
+
- Le débit reste à mesurer sur le serveur de production (amia).
|
|
32
|
+
|
|
6
33
|
## [0.1.0] — 2026-10-25
|
|
7
34
|
|
|
8
35
|
### Ajouté
|
package/README.md
CHANGED
|
@@ -11,8 +11,11 @@ Ce module dit si une voix a prononcé un texte connu, et où elle s'en écarte,
|
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
npm install @mostajs/prononciation
|
|
14
|
+
bash node_modules/@mostajs/prononciation/service/installer.sh # le service de reconnaissance (0.2.0), voir service/README.md
|
|
14
15
|
```
|
|
15
16
|
|
|
17
|
+
Le service de reconnaissance (Python, wav2vec2) est livré avec le module, dans `service/`. Il écoute en local, et `reconnaisseurHttp` en est le client.
|
|
18
|
+
|
|
16
19
|
## Exemple minimal
|
|
17
20
|
|
|
18
21
|
```js
|
|
@@ -23,7 +26,7 @@ import { readFileSync } from 'node:fs';
|
|
|
23
26
|
const p = createPrononciation({
|
|
24
27
|
lexique: combinerLexiques(creerLexique({ BOUMERDES: 'b u m ɛ ɹ d ɛ s' }),
|
|
25
28
|
lexiqueCmudict(readFileSync('cmudict.dict', 'utf8'))),
|
|
26
|
-
reconnaisseur: reconnaisseurHttp({ url: 'http://127.0.0.1:
|
|
29
|
+
reconnaisseur: reconnaisseurHttp({ url: 'http://127.0.0.1:8762/reconnaitre', type: 'audio/wav' }),
|
|
27
30
|
note: NOTE_APPRISE_1,
|
|
28
31
|
});
|
|
29
32
|
|
|
@@ -57,7 +60,7 @@ Les poids sont appris sur des voix d'apprenants chinois. Il faut les remesurer s
|
|
|
57
60
|
## Vérifier
|
|
58
61
|
|
|
59
62
|
```bash
|
|
60
|
-
npm test #
|
|
63
|
+
npm test # 23 essais (mjs-unit) + 9 essais du service (Python), parités comprises
|
|
61
64
|
node examples/verifier.mjs # trois vraies phrases, par le vrai adaptateur HTTP
|
|
62
65
|
```
|
|
63
66
|
|
package/llms.txt
CHANGED
|
@@ -42,6 +42,17 @@ Port reconnaisseur { reconnaitre({ audio, attendus }) → Promise<{ entendus: st
|
|
|
42
42
|
Contrat HTTP : POST <url>, corps = octets de l'audio ; en-tête x-attendus = encodeURIComponent(JSON des sons attendus) ;
|
|
43
43
|
réponse 200 JSON { entendus, traits?, duree? }.
|
|
44
44
|
|
|
45
|
+
SERVICE DE RECONNAISSANCE (0.2.0, service/, Python ≥ 3.10)
|
|
46
|
+
Installer : bash service/installer.sh (venv, dépendances FIGÉES, wav2vec2 à la révision ae45363b ; PRONONCIATION_HF_HOME)
|
|
47
|
+
Lancer : HF_HOME=… service/.venv/bin/python service/serveur.py --local-seulement → écoute http://127.0.0.1:8762
|
|
48
|
+
options --hote 127.0.0.1, --port 8762 (0 = libre), --fils 2, --octets 2000000, --secondes 15
|
|
49
|
+
POST /reconnaitre → { entendus, traits, duree, regle: 'stricte-2', modele } ; GET /sante → { ok, modele, revision, regle, classes }
|
|
50
|
+
Refus { erreur, message } : ATTENDUS 400, AUDIO_ABSENT 400 (vide ou < 0,25 s), LONGUEUR 411, TROP_GROS 413,
|
|
51
|
+
TROP_LONG 413, FORMAT 415 (WAV/FLAC/OGG ; pas WebM), CHEMIN 404, METHODE 405, INTERNE 500 → côté JS : RECONNAISSEUR 502
|
|
52
|
+
Une inférence à la fois ; audio jamais écrit ; journal = comptes et durées seulement.
|
|
53
|
+
Parité : traits identiques à l'apprentissage (T-SRV-9, écart 0) ; note de Python de bout en bout (T-PRO-23).
|
|
54
|
+
Piège : la révision du modèle et les versions sont figées — en changer, c'est changer la note.
|
|
55
|
+
|
|
45
56
|
NOTES ET SEUILS
|
|
46
57
|
- Simple (aucune note configurée) : son = 2 si retrouvé, sinon 0 ; phrase = 10 × part retrouvée. Seuils : phrase 6, mot 1, son 1.
|
|
47
58
|
- Apprise (note: NOTE_APPRISE_1) :
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/prononciation",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Compare une voix à un texte connu : sons attendus par un lexique injecté, sons entendus par un reconnaisseur injecté, alignement, note par son, mot et phrase (note apprise ou simple), verdict, et les sons qui résistent.",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Compare une voix à un texte connu : sons attendus par un lexique injecté, sons entendus par un reconnaisseur injecté, alignement, note par son, mot et phrase (note apprise ou simple), verdict, et les sons qui résistent. Avec son service de reconnaissance (Python, wav2vec2), local.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
7
|
"type": "module",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"src",
|
|
15
|
+
"service",
|
|
16
|
+
"!service/.venv",
|
|
17
|
+
"!service/**/__pycache__",
|
|
15
18
|
"llms.txt",
|
|
16
19
|
"README.md",
|
|
17
20
|
"CHANGELOG.md",
|
|
@@ -33,7 +36,9 @@
|
|
|
33
36
|
"phoneme",
|
|
34
37
|
"speech",
|
|
35
38
|
"english",
|
|
36
|
-
"cmudict"
|
|
39
|
+
"cmudict",
|
|
40
|
+
"wav2vec2",
|
|
41
|
+
"speech-recognition"
|
|
37
42
|
],
|
|
38
43
|
"dependencies": {},
|
|
39
44
|
"devDependencies": {
|
|
@@ -59,7 +64,8 @@
|
|
|
59
64
|
"interop": [
|
|
60
65
|
"IPA (Alphabet phonétique international)",
|
|
61
66
|
"ARPAbet / CMUdict",
|
|
62
|
-
"speechocean762 (échelles de notes 0-2 par son, 0-10 par phrase)"
|
|
67
|
+
"speechocean762 (échelles de notes 0-2 par son, 0-10 par phrase)",
|
|
68
|
+
"HTTP : POST /reconnaitre (x-attendus), GET /sante — service/README.md"
|
|
63
69
|
],
|
|
64
70
|
"ne_fait_pas": {
|
|
65
71
|
"faire entendre une phrase": "@mostajs/voix",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Service de reconnaissance de `@mostajs/prononciation`
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later · Python ≥ 3.10
|
|
4
|
+
|
|
5
|
+
Il reçoit l'audio d'un élève et les sons attendus. Il rend les sons entendus, 8 traits par son attendu et la durée, exactement comme pour l'apprentissage de `note-apprise-1`. `reconnaisseurHttp` en est le client.
|
|
6
|
+
|
|
7
|
+
## Installer
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bash node_modules/@mostajs/prononciation/service/installer.sh
|
|
11
|
+
# ou, pour ranger le modèle (≈ 1,2 Go) ailleurs que dans ~/.cache/huggingface :
|
|
12
|
+
PRONONCIATION_HF_HOME=/srv/modeles bash node_modules/@mostajs/prononciation/service/installer.sh
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Installé depuis npm, le service vit dans `node_modules/`, que `npm ci` efface à chaque déploiement : posez `PRONONCIATION_VENV` (et `PRONONCIATION_HF_HOME`) **hors du projet**, pour n'installer qu'une fois :
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
PRONONCIATION_VENV=/srv/prononciation-venv PRONONCIATION_HF_HOME=/srv/modeles bash node_modules/@mostajs/prononciation/service/installer.sh
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
L'installeur fait trois choses :
|
|
22
|
+
- il crée `service/.venv` et y installe les dépendances **figées** (`requirements.txt` : torch 2.4.1 CPU, transformers 4.44.2…) ;
|
|
23
|
+
- il télécharge `facebook/wav2vec2-lv-60-espeak-cv-ft` à sa **révision figée** (`ae45363b`) ;
|
|
24
|
+
- il vérifie que le modèle se charge sans réseau.
|
|
25
|
+
|
|
26
|
+
Une autre version des dépendances ou du modèle donne d'autres traits, et la note apprise ne vaut plus ses 0,72.
|
|
27
|
+
|
|
28
|
+
## Lancer
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
HF_HOME=/srv/modeles service/.venv/bin/python service/serveur.py --local-seulement
|
|
32
|
+
# écoute http://127.0.0.1:8762
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| option | défaut | rôle |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `--hote` | `127.0.0.1` (`PRONONCIATION_HOTE`) | écoute locale : seule l'application le joint |
|
|
38
|
+
| `--port` | `8762` (`PRONONCIATION_PORT`) | `0` : un port libre, annoncé sur la sortie |
|
|
39
|
+
| `--fils` | `2` (`PRONONCIATION_FILS`) | fils de calcul du modèle |
|
|
40
|
+
| `--octets` | `2000000` | taille maximale du corps |
|
|
41
|
+
| `--secondes` | `15` | durée maximale de l'audio |
|
|
42
|
+
| `--local-seulement` | non | ne rien télécharger ; sortie 3 si le modèle manque |
|
|
43
|
+
|
|
44
|
+
Côté application :
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
reconnaisseurHttp({ url: 'http://127.0.0.1:8762/reconnaitre', type: 'audio/wav' })
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Le contrat
|
|
51
|
+
|
|
52
|
+
- `POST /reconnaitre` : le corps est l'audio (WAV, FLAC ou OGG, de 0,25 à 15 s) ; l'en-tête `x-attendus` porte les sons attendus, en JSON encodé. Réponse 200 : `{ entendus, traits, duree, regle, modele }`.
|
|
53
|
+
- `GET /sante` : `{ ok, modele, revision, regle, classes }`.
|
|
54
|
+
- Refus `{ erreur, message }` :
|
|
55
|
+
|
|
56
|
+
| code | statut | quand |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `ATTENDUS` | 400 | `x-attendus` absent, illisible, vide, plus de 400 sons, ou un son de plus de 4 signes |
|
|
59
|
+
| `AUDIO_ABSENT` | 400 | corps vide, ou moins de 0,25 s |
|
|
60
|
+
| `LONGUEUR` | 411 | pas de `content-length` |
|
|
61
|
+
| `TROP_GROS` | 413 | corps au-delà de `--octets` (refusé sans être lu) |
|
|
62
|
+
| `TROP_LONG` | 413 | audio au-delà de `--secondes` |
|
|
63
|
+
| `FORMAT` | 415 | audio illisible (le WebM/Opus des navigateurs ne l'est pas : enregistrer en WAV) |
|
|
64
|
+
| `CHEMIN`, `METHODE` | 404, 405 | autre adresse, autre méthode |
|
|
65
|
+
| `INTERNE` | 500 | toute autre faute |
|
|
66
|
+
|
|
67
|
+
## Ce qu'il ne garde pas
|
|
68
|
+
|
|
69
|
+
L'audio est lu en mémoire et n'est jamais écrit. Le journal, sur la sortie d'erreur, ne porte que des comptes et des durées : jamais l'audio, jamais les sons attendus (essai T-SRV-8).
|
|
70
|
+
|
|
71
|
+
## Débit
|
|
72
|
+
|
|
73
|
+
Une inférence à la fois ; les requêtes suivantes attendent. Mesure du 25/10/2026, poste de développement, 2 fils : **1,5 à 2 s de calcul pour 3 s d'audio**. Le débit sur le serveur de production reste à mesurer.
|
|
74
|
+
|
|
75
|
+
## Essais
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
PRONONCIATION_PYTHON=service/.venv/bin/python npm test # 23 essais JS + 9 essais du service
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
T-SRV-9 compare les traits du vrai modèle à ceux de l'apprentissage, sur cinq phrases de speechocean762 : l'écart est **nul**. T-PRO-23 lance le service et note trois phrases de bout en bout, par `reconnaisseurHttp` : on retrouve la note de Python.
|
package/service/audio.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@mostajs/prononciation — service de reconnaissance : octets → onde mono à 16 kHz, en mémoire.
|
|
3
|
+
|
|
4
|
+
@author Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
@license AGPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
libsndfile lit WAV, FLAC, OGG. Rien n'est écrit sur le disque : la voix d'un élève ne quitte pas la mémoire du
|
|
8
|
+
processus, et le journal n'en garde que la durée.
|
|
9
|
+
"""
|
|
10
|
+
import io
|
|
11
|
+
|
|
12
|
+
import soundfile as sf
|
|
13
|
+
import torch
|
|
14
|
+
import torchaudio.functional as AF
|
|
15
|
+
|
|
16
|
+
from erreurs import ErreurService
|
|
17
|
+
|
|
18
|
+
TAUX = 16000
|
|
19
|
+
SECONDES_MIN = 0.25 # en deçà, le modèle n'a pas assez de trames pour un mot
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def lire_audio(octets, secondes_max=15.0):
|
|
23
|
+
try:
|
|
24
|
+
with sf.SoundFile(io.BytesIO(octets)) as f:
|
|
25
|
+
if f.frames / f.samplerate > secondes_max:
|
|
26
|
+
raise ErreurService('TROP_LONG', f'audio de {f.frames / f.samplerate:.1f} s ; au plus {secondes_max:g} s')
|
|
27
|
+
if f.frames / f.samplerate < SECONDES_MIN:
|
|
28
|
+
raise ErreurService('AUDIO_ABSENT', f'audio de {f.frames / f.samplerate:.2f} s ; au moins {SECONDES_MIN:g} s')
|
|
29
|
+
onde, sr = f.read(dtype='float32'), f.samplerate
|
|
30
|
+
except ErreurService:
|
|
31
|
+
raise
|
|
32
|
+
except Exception:
|
|
33
|
+
raise ErreurService('FORMAT', 'audio illisible (WAV, FLAC ou OGG attendu)') from None
|
|
34
|
+
if onde.ndim > 1:
|
|
35
|
+
onde = onde.mean(axis=1)
|
|
36
|
+
if sr != TAUX:
|
|
37
|
+
onde = AF.resample(torch.from_numpy(onde), sr, TAUX).numpy()
|
|
38
|
+
return onde
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@mostajs/prononciation — service de reconnaissance : les refus nommés (plan §8.3).
|
|
3
|
+
|
|
4
|
+
@author Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
@license AGPL-3.0-or-later
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
STATUTS = {
|
|
9
|
+
'ATTENDUS': 400, 'AUDIO_ABSENT': 400, 'LONGUEUR': 411, 'TROP_GROS': 413, 'TROP_LONG': 413,
|
|
10
|
+
'FORMAT': 415, 'CHEMIN': 404, 'METHODE': 405, 'INTERNE': 500,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ErreurService(Exception):
|
|
15
|
+
"""Un refus du service : un code nommé, son statut HTTP, un message en français."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, code, message):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.code, self.statut, self.message = code, STATUTS[code], message
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# @mostajs/prononciation — installe le service de reconnaissance (rejouable, plan §8.2).
|
|
4
|
+
# Auteur : Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
#
|
|
6
|
+
# bash service/installer.sh
|
|
7
|
+
# PRONONCIATION_HF_HOME=/srv/modeles bash service/installer.sh # où ranger le modèle (≈ 1,2 Go)
|
|
8
|
+
# PRONONCIATION_VENV=/srv/prononciation-venv bash service/installer.sh # l'environnement Python HORS de node_modules
|
|
9
|
+
#
|
|
10
|
+
# 0.2.1 — installé depuis npm, le service vit dans node_modules/, que `npm ci` efface à chaque déploiement : poser
|
|
11
|
+
# PRONONCIATION_VENV (et PRONONCIATION_HF_HOME) hors du projet, pour n'installer qu'une fois.
|
|
12
|
+
#
|
|
13
|
+
# Crée service/.venv (Python ≥ 3.10), y installe les dépendances figées, télécharge le modèle à sa révision figée,
|
|
14
|
+
# puis vérifie qu'il se charge sans réseau. Rien d'autre n'est écrit.
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
ICI="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
PY="${PYTHON:-python3}"
|
|
18
|
+
"$PY" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)' || { echo "✗ Python ≥ 3.10 requis ($PY)"; exit 1; }
|
|
19
|
+
VENV="${PRONONCIATION_VENV:-$ICI/.venv}"
|
|
20
|
+
[ -x "$VENV/bin/python" ] || "$PY" -m venv "$VENV"
|
|
21
|
+
"$VENV/bin/pip" install -q --upgrade pip
|
|
22
|
+
"$VENV/bin/pip" install -q -r "$ICI/requirements.txt"
|
|
23
|
+
[ -n "${PRONONCIATION_HF_HOME:-}" ] && export HF_HOME="$PRONONCIATION_HF_HOME"
|
|
24
|
+
cd "$ICI"
|
|
25
|
+
"$VENV/bin/python" - <<'PY'
|
|
26
|
+
from huggingface_hub import hf_hub_download
|
|
27
|
+
import moteur
|
|
28
|
+
for f in moteur.FICHIERS:
|
|
29
|
+
hf_hub_download(moteur.MODELE, f, revision=moteur.REVISION)
|
|
30
|
+
assert moteur.modele_installe(), 'modèle absent après téléchargement'
|
|
31
|
+
m = moteur.Moteur(local_seulement=True)
|
|
32
|
+
print(f'✓ modèle {moteur.MODELE}@{moteur.REVISION[:8]} chargé : {m.classes} classes de sons, règle {m.regle}')
|
|
33
|
+
PY
|
|
34
|
+
echo "✓ installé. Lancer : ${PRONONCIATION_HF_HOME:+HF_HOME=$PRONONCIATION_HF_HOME }$VENV/bin/python $ICI/serveur.py --local-seulement"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@mostajs/prononciation — service de reconnaissance : le modèle acoustique, chargé une fois.
|
|
3
|
+
|
|
4
|
+
@author Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
@license AGPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
wav2vec2 phonémique (Apache-2.0), à la révision qui a servi à apprendre note-apprise-1. Une inférence à la fois :
|
|
8
|
+
deux en parallèle sur les mêmes cœurs se ralentissent l'une l'autre (dossier produit, 11 §3).
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
from huggingface_hub import hf_hub_download
|
|
16
|
+
from transformers import Wav2Vec2FeatureExtractor, Wav2Vec2ForCTC
|
|
17
|
+
from transformers.utils import logging as journal_transformers
|
|
18
|
+
|
|
19
|
+
# transformers 4.44 annonce à tort des poids « réinitialisés » (weight_g/weight_v → parametrizations) : ils sont bien
|
|
20
|
+
# chargés — T-SRV-9 retrouve les traits d'apprentissage à l'identique. L'avertissement égarerait l'exploitant.
|
|
21
|
+
journal_transformers.set_verbosity_error()
|
|
22
|
+
|
|
23
|
+
import sons
|
|
24
|
+
import traits
|
|
25
|
+
|
|
26
|
+
MODELE = 'facebook/wav2vec2-lv-60-espeak-cv-ft'
|
|
27
|
+
REVISION = 'ae45363bf3413b374fecd9dc8bc1df0e24c3b7f4'
|
|
28
|
+
FICHIERS = ('config.json', 'preprocessor_config.json', 'vocab.json', 'pytorch_model.bin')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def modele_installe():
|
|
32
|
+
"""Vrai si la révision figée du modèle est dans le cache local (HF_HOME), sans réseau."""
|
|
33
|
+
try:
|
|
34
|
+
for f in FICHIERS:
|
|
35
|
+
hf_hub_download(MODELE, f, revision=REVISION, local_files_only=True)
|
|
36
|
+
return True
|
|
37
|
+
except Exception:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Moteur:
|
|
42
|
+
modele, revision, regle = MODELE, REVISION, sons.REGLE
|
|
43
|
+
|
|
44
|
+
def __init__(self, fils=2, local_seulement=False):
|
|
45
|
+
torch.set_num_threads(fils)
|
|
46
|
+
o = {'revision': REVISION, 'local_files_only': local_seulement}
|
|
47
|
+
vocab = json.load(open(hf_hub_download(MODELE, 'vocab.json', **o), encoding='utf-8'))
|
|
48
|
+
self.noms, self.membre, self.matrice, self.blanc = sons.classes_du_vocabulaire(vocab)
|
|
49
|
+
self.classes = len(self.noms)
|
|
50
|
+
self.id2tok = {i: t for t, i in vocab.items()}
|
|
51
|
+
self.proc = Wav2Vec2FeatureExtractor.from_pretrained(MODELE, **o)
|
|
52
|
+
self.model = Wav2Vec2ForCTC.from_pretrained(MODELE, **o).eval()
|
|
53
|
+
self.verrou = threading.Lock()
|
|
54
|
+
|
|
55
|
+
def reconnaitre(self, onde, attendus):
|
|
56
|
+
"""onde : float32 mono 16 kHz ; attendus : sons → { entendus, traits, duree, calcul }."""
|
|
57
|
+
with self.verrou:
|
|
58
|
+
t0 = time.perf_counter()
|
|
59
|
+
with torch.inference_mode():
|
|
60
|
+
logits = self.model(self.proc(onde, sampling_rate=16000, return_tensors='pt').input_values).logits[0]
|
|
61
|
+
lp = torch.log_softmax(logits, dim=-1)
|
|
62
|
+
lp_cls = torch.log(lp.exp() @ self.matrice + 1e-12)
|
|
63
|
+
lp_blanc = lp[:, self.blanc]
|
|
64
|
+
bruts, prec = [], None
|
|
65
|
+
for i in torch.argmax(logits, dim=-1).tolist(): # décodage glouton
|
|
66
|
+
if i != prec and i != self.blanc:
|
|
67
|
+
tk = self.id2tok.get(i, '')
|
|
68
|
+
if tk.strip() and tk not in sons.SPECIAUX:
|
|
69
|
+
bruts.append(tk)
|
|
70
|
+
prec = i
|
|
71
|
+
r = {'entendus': sons.entendre(bruts), 'traits': traits.caracteristiques(lp_cls, lp_blanc, attendus, self.membre),
|
|
72
|
+
'duree': len(onde) / 16000}
|
|
73
|
+
r['calcul'] = time.perf_counter() - t0
|
|
74
|
+
return r
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @mostajs/prononciation — service de reconnaissance : dépendances FIGÉES (plan §8.2).
|
|
2
|
+
# Ce sont celles qui ont produit les traits d'apprentissage de note-apprise-1 : une autre version est une autre mesure.
|
|
3
|
+
# Auteur : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
|
5
|
+
torch==2.4.1+cpu
|
|
6
|
+
torchaudio==2.4.1+cpu
|
|
7
|
+
transformers==4.44.2
|
|
8
|
+
huggingface_hub==0.24.6
|
|
9
|
+
numpy==1.26.4
|
|
10
|
+
soundfile==0.12.1
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
@mostajs/prononciation — service de reconnaissance : le serveur HTTP (plan §8.3).
|
|
4
|
+
|
|
5
|
+
@author Dr Hamid MADANI <drmdh@msn.com>
|
|
6
|
+
@license AGPL-3.0-or-later
|
|
7
|
+
|
|
8
|
+
POST /reconnaitre corps = l'audio ; x-attendus = JSON des sons attendus (encodeURIComponent)
|
|
9
|
+
→ 200 { entendus, traits, duree, regle, modele }
|
|
10
|
+
GET /sante → 200 { ok, modele, revision, regle, classes }
|
|
11
|
+
Tout refus : { erreur, message } avec son statut. Écoute locale par défaut : seule l'application le joint.
|
|
12
|
+
Le journal ne porte que des comptes et des durées — jamais l'audio, jamais les sons attendus.
|
|
13
|
+
|
|
14
|
+
Lancer : python serveur.py [--hote 127.0.0.1] [--port 8762] [--fils 2] [--octets 2000000] [--secondes 15]
|
|
15
|
+
"""
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import signal
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
import urllib.parse
|
|
23
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
24
|
+
|
|
25
|
+
from audio import lire_audio
|
|
26
|
+
from erreurs import ErreurService
|
|
27
|
+
|
|
28
|
+
SONS_MAX, SON_LONGUEUR_MAX = 400, 4
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def lire_attendus(entete):
|
|
32
|
+
if not entete:
|
|
33
|
+
raise ErreurService('ATTENDUS', 'en-tête x-attendus absent')
|
|
34
|
+
try:
|
|
35
|
+
a = json.loads(urllib.parse.unquote(entete))
|
|
36
|
+
except Exception:
|
|
37
|
+
raise ErreurService('ATTENDUS', 'x-attendus : JSON illisible') from None
|
|
38
|
+
if not isinstance(a, list) or not 1 <= len(a) <= SONS_MAX:
|
|
39
|
+
raise ErreurService('ATTENDUS', f'x-attendus : une liste de 1 à {SONS_MAX} sons')
|
|
40
|
+
if not all(isinstance(s, str) and 1 <= len(s) <= SON_LONGUEUR_MAX and not any(c.isspace() for c in s) for s in a):
|
|
41
|
+
raise ErreurService('ATTENDUS', f'x-attendus : chaque son est une chaîne de 1 à {SON_LONGUEUR_MAX} signes')
|
|
42
|
+
return a
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def creer_serveur(moteur, hote='127.0.0.1', port=8762, octets_max=2_000_000, secondes_max=15.0, journal=None):
|
|
46
|
+
ecrire = journal or (lambda m: print(time.strftime('%Y-%m-%dT%H:%M:%S'), m, file=sys.stderr, flush=True))
|
|
47
|
+
|
|
48
|
+
class Gestion(BaseHTTPRequestHandler):
|
|
49
|
+
server_version = 'prononciation-reconnaissance'
|
|
50
|
+
sys_version = ''
|
|
51
|
+
|
|
52
|
+
def log_message(self, *args): # pas de journal d'accès : il porterait les en-têtes
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def repondre(self, statut, corps):
|
|
56
|
+
b = json.dumps(corps, ensure_ascii=False).encode('utf-8')
|
|
57
|
+
self.send_response(statut)
|
|
58
|
+
self.send_header('content-type', 'application/json; charset=utf-8')
|
|
59
|
+
self.send_header('content-length', str(len(b)))
|
|
60
|
+
self.send_header('connection', 'close')
|
|
61
|
+
self.end_headers()
|
|
62
|
+
self.wfile.write(b)
|
|
63
|
+
self.close_connection = True
|
|
64
|
+
|
|
65
|
+
def refuser(self, e):
|
|
66
|
+
ecrire(f'refus {e.code} ({e.statut})')
|
|
67
|
+
self.repondre(e.statut, {'erreur': e.code, 'message': e.message})
|
|
68
|
+
|
|
69
|
+
def do_GET(self):
|
|
70
|
+
if self.path == '/sante':
|
|
71
|
+
return self.repondre(200, {'ok': True, 'modele': moteur.modele, 'revision': moteur.revision,
|
|
72
|
+
'regle': moteur.regle, 'classes': moteur.classes})
|
|
73
|
+
self.refuser(ErreurService('METHODE', 'POST attendu') if self.path == '/reconnaitre'
|
|
74
|
+
else ErreurService('CHEMIN', f'adresse inconnue ; /reconnaitre ou /sante'))
|
|
75
|
+
|
|
76
|
+
def autre(self):
|
|
77
|
+
self.refuser(ErreurService('METHODE', 'POST /reconnaitre ou GET /sante'))
|
|
78
|
+
|
|
79
|
+
do_PUT = do_DELETE = do_PATCH = autre
|
|
80
|
+
|
|
81
|
+
def do_POST(self):
|
|
82
|
+
try:
|
|
83
|
+
if self.path != '/reconnaitre':
|
|
84
|
+
raise ErreurService('CHEMIN', 'adresse inconnue ; /reconnaitre ou /sante')
|
|
85
|
+
attendus = lire_attendus(self.headers.get('x-attendus'))
|
|
86
|
+
longueur = self.headers.get('content-length')
|
|
87
|
+
if longueur is None or not longueur.isdigit():
|
|
88
|
+
raise ErreurService('LONGUEUR', 'content-length attendu')
|
|
89
|
+
n = int(longueur)
|
|
90
|
+
if n == 0:
|
|
91
|
+
raise ErreurService('AUDIO_ABSENT', 'corps vide : l’audio est attendu')
|
|
92
|
+
if n > octets_max: # refusé SANS lire : la borne n'est jamais dépassée en mémoire
|
|
93
|
+
raise ErreurService('TROP_GROS', f'{n} octets ; au plus {octets_max}')
|
|
94
|
+
onde = lire_audio(self.rfile.read(n), secondes_max)
|
|
95
|
+
r = moteur.reconnaitre(onde, attendus)
|
|
96
|
+
ecrire(f'reconnu : {len(attendus)} sons attendus, {len(r["entendus"])} entendus, '
|
|
97
|
+
f'{r["duree"]:.2f} s d’audio, {r.get("calcul", 0.0):.2f} s de calcul')
|
|
98
|
+
self.repondre(200, {'entendus': r['entendus'], 'traits': r['traits'], 'duree': r['duree'],
|
|
99
|
+
'regle': moteur.regle, 'modele': moteur.modele})
|
|
100
|
+
except ErreurService as e:
|
|
101
|
+
self.refuser(e)
|
|
102
|
+
except Exception:
|
|
103
|
+
self.refuser(ErreurService('INTERNE', 'faute interne du service'))
|
|
104
|
+
|
|
105
|
+
srv = ThreadingHTTPServer((hote, port), Gestion)
|
|
106
|
+
srv.daemon_threads = True
|
|
107
|
+
return srv
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main():
|
|
111
|
+
ap = argparse.ArgumentParser(description='Service de reconnaissance de @mostajs/prononciation')
|
|
112
|
+
ap.add_argument('--hote', default=os.environ.get('PRONONCIATION_HOTE', '127.0.0.1'))
|
|
113
|
+
ap.add_argument('--port', type=int, default=int(os.environ.get('PRONONCIATION_PORT', '8762')))
|
|
114
|
+
ap.add_argument('--fils', type=int, default=int(os.environ.get('PRONONCIATION_FILS', '2')))
|
|
115
|
+
ap.add_argument('--octets', type=int, default=2_000_000)
|
|
116
|
+
ap.add_argument('--secondes', type=float, default=15.0)
|
|
117
|
+
ap.add_argument('--local-seulement', action='store_true', help='ne rien télécharger : le modèle doit être installé')
|
|
118
|
+
a = ap.parse_args()
|
|
119
|
+
import moteur as m
|
|
120
|
+
if a.local_seulement and not m.modele_installe():
|
|
121
|
+
print(f'modèle {m.MODELE}@{m.REVISION[:8]} non installé : service/installer.sh', file=sys.stderr)
|
|
122
|
+
sys.exit(3)
|
|
123
|
+
srv = creer_serveur(m.Moteur(fils=a.fils, local_seulement=a.local_seulement), a.hote, a.port, a.octets, a.secondes)
|
|
124
|
+
signal.signal(signal.SIGTERM, lambda *_: (srv.server_close(), sys.exit(0)))
|
|
125
|
+
print(f'écoute http://{srv.server_address[0]}:{srv.server_address[1]}', flush=True)
|
|
126
|
+
srv.serve_forever()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == '__main__':
|
|
130
|
+
main()
|
package/service/sons.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@mostajs/prononciation — service de reconnaissance : jetons du modèle → classes de sons (règle stricte-2).
|
|
3
|
+
|
|
4
|
+
@author Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
@license AGPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
Repris de mesure_moteur_speechocean762.py (dossier produit), la règle qui a servi à apprendre note-apprise-1.
|
|
8
|
+
Le cœur JS (src/lib/notation.js, normaliserJetons) applique la même : l'essai T-SRV-1 les compare jeton par jeton
|
|
9
|
+
sur tout le vocabulaire du modèle. Corrections de notation seulement : longueur, accent, aspiration, tons chinois,
|
|
10
|
+
symboles composés (ɑːɹ = ɑ + ɹ).
|
|
11
|
+
"""
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
|
|
16
|
+
REGLE = 'stricte-2'
|
|
17
|
+
|
|
18
|
+
CLASSES = {'ə': 'ʌ', 'ɐ': 'ʌ', 'ɚ': 'ɜ', 'ɝ': 'ɜ', 'əʊ': 'oʊ', 'e': 'ɛ', 'ɒ': 'ɑ', 'g': 'ɡ',
|
|
19
|
+
'r': 'ɹ', 'ɾ': 't', 'ɫ': 'l', 'ᵻ': 'ɪ', 'ɪ̈': 'ɪ', 'o': 'oʊ', 'tS': 'tʃ', 'dZ': 'dʒ',
|
|
20
|
+
'th': 't', 'kh': 'k', 'ph': 'p', 'e̞': 'ɛ', 'o̞': 'oʊ', 'a': 'æ', 'ä': 'ɑ', 'l̩': 'l', 'n̩': 'n'}
|
|
21
|
+
COMPOSES = {'ɑɹ': ['ɑ', 'ɹ'], 'ɔɹ': ['ɔ', 'ɹ'], 'oɹ': ['ɔ', 'ɹ'], 'ɪɹ': ['ɪ', 'ɹ'], 'ɛɹ': ['ɛ', 'ɹ'],
|
|
22
|
+
'ʊɹ': ['ʊ', 'ɹ'], 'əl': ['ʌ', 'l'], 'aɪə': ['aɪ', 'ʌ'], 'aɪɚ': ['aɪ', 'ɜ'], 'ju': ['j', 'u'],
|
|
23
|
+
'ja': ['j', 'ɑ'], 'iə': ['i', 'ʌ'], 'eə': ['ɛ', 'ʌ'], 'ʊə': ['ʊ', 'ʌ']}
|
|
24
|
+
VOYELLES = set('aeiouæɑɒɔəɛɜɪʊʌy')
|
|
25
|
+
SPECIAUX = {'<s>', '</s>', '<unk>', '|', ' ', '<pad>'}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def nettoyer(p):
|
|
29
|
+
p = re.sub(r'[ːˈˌ:."^\[ʰʲ]', '', p)
|
|
30
|
+
p = re.sub(r'[1-5]+$', '', p) # tons chinois notés par chiffre
|
|
31
|
+
if len(p) > 1 and p.endswith('ɜ') and p[0] in VOYELLES:
|
|
32
|
+
p = p[:-1] # ton 3 noté ɜ (iɜ, uɜ, əɜ…)
|
|
33
|
+
return p
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def entendre(jetons):
|
|
37
|
+
"""Jetons bruts du modèle → classes de sons comparables (liste)."""
|
|
38
|
+
out = []
|
|
39
|
+
for j in jetons:
|
|
40
|
+
p = nettoyer(j)
|
|
41
|
+
if not p:
|
|
42
|
+
continue
|
|
43
|
+
for q in COMPOSES.get(p, [p]):
|
|
44
|
+
out.append(CLASSES.get(q, q))
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def classes_du_vocabulaire(vocab):
|
|
49
|
+
"""vocab {jeton: indice} → (noms des classes, {classe: colonne}, matrice [jetons × classes], indice du blanc).
|
|
50
|
+
Un symbole composé (ɑːɹ) compte pour CHACUN de ses sons."""
|
|
51
|
+
noms, membre = [], {}
|
|
52
|
+
for tok in vocab:
|
|
53
|
+
if tok in SPECIAUX:
|
|
54
|
+
continue
|
|
55
|
+
for c in entendre([tok]):
|
|
56
|
+
if c not in membre:
|
|
57
|
+
membre[c] = len(noms)
|
|
58
|
+
noms.append(c)
|
|
59
|
+
matrice = torch.zeros(len(vocab), len(noms))
|
|
60
|
+
for tok, i in vocab.items():
|
|
61
|
+
if tok not in SPECIAUX:
|
|
62
|
+
for c in entendre([tok]):
|
|
63
|
+
matrice[i, membre[c]] = 1.0
|
|
64
|
+
return noms, membre, matrice, vocab['<pad>']
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@mostajs/prononciation — service de reconnaissance : les 8 traits par son attendu (GOP).
|
|
3
|
+
|
|
4
|
+
@author Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
@license AGPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
Repris tel quel d'extraire_gop.py (dossier produit), qui a produit les traits d'apprentissage de note-apprise-1 :
|
|
8
|
+
l'alignement forcé CTC place chaque son attendu sur ses trames, puis, par son :
|
|
9
|
+
gop_moy, gop_max log-probabilité moyenne et maximale de la classe attendue sur ses trames ;
|
|
10
|
+
fenetre_max maximum sur une fenêtre élargie (fin du son précédent → début du suivant : le CTC est « pointu ») ;
|
|
11
|
+
lpr_moy, lpr_max écart au meilleur concurrent ;
|
|
12
|
+
duree, fenetre en trames de 20 ms ;
|
|
13
|
+
blanc_moy probabilité moyenne du « rien ».
|
|
14
|
+
Un son que le modèle ne connaît pas reçoit le plancher.
|
|
15
|
+
"""
|
|
16
|
+
import numpy as np
|
|
17
|
+
import torch
|
|
18
|
+
import torchaudio.functional as AF
|
|
19
|
+
|
|
20
|
+
PLANCHER = float(np.log(1e-8))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def caracteristiques(lp_cls, lp_blanc, attendus, membre):
|
|
24
|
+
"""lp_cls [T, C] log-proba par classe ; lp_blanc [T] ; attendus : sons (suffixe 0 possible) → [[8 traits]]."""
|
|
25
|
+
T = lp_cls.shape[0]
|
|
26
|
+
bases = [a[:-1] if a.endswith('0') else a for a in attendus]
|
|
27
|
+
cibles = [membre.get(b, -1) for b in bases]
|
|
28
|
+
# émission réduite : colonne 0 = blanc, puis une colonne par position attendue
|
|
29
|
+
em = torch.full((T, len(cibles) + 1), PLANCHER)
|
|
30
|
+
em[:, 0] = lp_blanc
|
|
31
|
+
for j, c in enumerate(cibles):
|
|
32
|
+
if c >= 0:
|
|
33
|
+
em[:, j + 1] = lp_cls[:, c]
|
|
34
|
+
em = em - torch.logsumexp(em, dim=1, keepdim=True)
|
|
35
|
+
try:
|
|
36
|
+
ali, _ = AF.forced_align(em[None], torch.arange(1, len(cibles) + 1, dtype=torch.int32)[None], blank=0)
|
|
37
|
+
spans = AF.merge_tokens(ali[0], torch.zeros(T))
|
|
38
|
+
bornes = [(s.start, s.end) for s in spans]
|
|
39
|
+
if len(bornes) != len(cibles):
|
|
40
|
+
raise RuntimeError('alignement incomplet')
|
|
41
|
+
except Exception:
|
|
42
|
+
pas = T / max(1, len(cibles)) # trop court pour l'alignement : partage égal
|
|
43
|
+
bornes = [(int(j * pas), max(int(j * pas) + 1, int((j + 1) * pas))) for j in range(len(cibles))]
|
|
44
|
+
# Plus de sons que de trames : le partage déborde de l'audio. On ramène chaque segment sur la dernière trame
|
|
45
|
+
# (0.2.0 ; extraire_gop.py n'a jamais rencontré le cas sur le corpus). Un segment dans l'audio n'est pas touché.
|
|
46
|
+
bornes = [(d, f) if d < T else (T - 1, T) for d, f in bornes]
|
|
47
|
+
meilleur = lp_cls.max(dim=1).values
|
|
48
|
+
out = []
|
|
49
|
+
for j, (c, (d, f)) in enumerate(zip(cibles, bornes)):
|
|
50
|
+
wd = bornes[j - 1][1] if j else 0
|
|
51
|
+
wf = bornes[j + 1][0] if j + 1 < len(bornes) else T
|
|
52
|
+
wf = max(wf, f)
|
|
53
|
+
wd = min(wd, d)
|
|
54
|
+
if c >= 0:
|
|
55
|
+
seg, win = lp_cls[d:f, c], lp_cls[wd:wf, c]
|
|
56
|
+
gop_moy, gop_max, win_max = float(seg.mean()), float(seg.max()), float(win.max())
|
|
57
|
+
lpr = seg - meilleur[d:f]
|
|
58
|
+
lpr_moy, lpr_max = float(lpr.mean()), float(lpr.max())
|
|
59
|
+
else:
|
|
60
|
+
gop_moy = gop_max = win_max = lpr_moy = lpr_max = PLANCHER
|
|
61
|
+
out.append([gop_moy, gop_max, win_max, lpr_moy, lpr_max, f - d, wf - wd, float(lp_blanc[d:f].mean())])
|
|
62
|
+
return out
|
package/src/lib/lexique.js
CHANGED
|
@@ -23,12 +23,14 @@ export function creerLexique(table = {}) {
|
|
|
23
23
|
export function lexiqueCmudict(texte) {
|
|
24
24
|
if (typeof texte !== 'string') refus('CONFIGURATION', 'lexiqueCmudict : le texte du fichier est attendu');
|
|
25
25
|
const table = {};
|
|
26
|
-
for (const
|
|
27
|
-
if (
|
|
26
|
+
for (const brute of texte.split(/\r?\n/)) {
|
|
27
|
+
if (brute.startsWith(';;;')) continue;
|
|
28
|
+
const ligne = brute.split('#')[0]; // 0.2.1 : « … R G # place, danish » → commentaire
|
|
29
|
+
if (!ligne.trim()) continue;
|
|
28
30
|
const [mot, ...sons] = ligne.trim().split(/\s+/);
|
|
29
31
|
if (!sons.length || /\(\d+\)$/.test(mot)) continue; // variante : TOMATO(2)
|
|
30
32
|
const cle = mot.toUpperCase();
|
|
31
|
-
if (!(cle in table)) table[cle] = sons.map((s) => s.toUpperCase())
|
|
33
|
+
if (!(cle in table)) table[cle] = sons.map((s) => s.toUpperCase());
|
|
32
34
|
}
|
|
33
35
|
return creerLexique(table);
|
|
34
36
|
}
|