@mostajs/prononciation 0.0.0-stage → 0.2.0
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 +43 -0
- package/LICENSE +15 -0
- package/README.md +72 -2
- package/llms.txt +79 -0
- package/package.json +77 -4
- package/service/README.md +75 -0
- package/service/audio.py +38 -0
- package/service/erreurs.py +19 -0
- package/service/installer.sh +29 -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/adapters/fixe.js +12 -0
- package/src/adapters/http.js +37 -0
- package/src/index.js +13 -0
- package/src/lib/alignement.js +36 -0
- package/src/lib/erreurs.js +38 -0
- package/src/lib/lexique.js +45 -0
- package/src/lib/notation.js +71 -0
- package/src/lib/note.js +98 -0
- package/src/lib/resistances.js +25 -0
- package/src/lib/texte.js +11 -0
- package/src/poids/note-apprise-1.js +5 -0
- package/src/prononciation.js +100 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog — @mostajs/prononciation
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
Format : Keep a Changelog ; versions : SemVer.
|
|
5
|
+
|
|
6
|
+
## [0.2.0] — 2026-10-25
|
|
7
|
+
|
|
8
|
+
### Ajouté
|
|
9
|
+
- **Le service de reconnaissance** (`service/`, Python ≥ 3.10), livré avec le module :
|
|
10
|
+
- `POST /reconnaitre` sert le contrat de `reconnaisseurHttp` ; `GET /sante` le décrit ;
|
|
11
|
+
- wav2vec2 phonémique à révision figée, et dépendances figées (`requirements.txt`) ;
|
|
12
|
+
- alignement forcé CTC et 8 traits par son attendu, repris d'`extraire_gop.py`.
|
|
13
|
+
- **Refus nommés** : ATTENDUS, AUDIO_ABSENT, LONGUEUR, TROP_GROS, TROP_LONG, FORMAT, CHEMIN, METHODE, INTERNE.
|
|
14
|
+
- **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.
|
|
15
|
+
- **`service/installer.sh`** : rejouable, il vérifie que le modèle se charge sans réseau.
|
|
16
|
+
- **Essais** :
|
|
17
|
+
- T-SRV-1 à 9 (unittest). Parité avec l'apprentissage sur cinq phrases : écart de traits **nul**.
|
|
18
|
+
- T-PRO-23 : le service réel derrière `reconnaisseurHttp` rend la note de Python.
|
|
19
|
+
|
|
20
|
+
### Corrigé
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
### Points ouverts
|
|
24
|
+
- Le WebM/Opus enregistré par les navigateurs n'est pas lu. Il faut l'enregistreur WAV de `@mostajs/media` (B-03).
|
|
25
|
+
- Le débit reste à mesurer sur le serveur de production (amia).
|
|
26
|
+
|
|
27
|
+
## [0.1.0] — 2026-10-25
|
|
28
|
+
|
|
29
|
+
### Ajouté
|
|
30
|
+
- `createPrononciation({ lexique, reconnaisseur, note?, seuils?, regle?, limites? })`, avec `evaluer({ texte, audio })` et `attendus(texte)`.
|
|
31
|
+
- Lexiques : `creerLexique` (IPA ou ARPAbet), `lexiqueCmudict`, `combinerLexiques` ; `arpaVersIpa`, `motsDe`.
|
|
32
|
+
- Alignement `aligner` : états juste, faux, omis ; sons en face ; insertions. Règles `stricte-2` et `tolerante-2`, et `normaliserJetons` pour les sorties d'espeak. Parité avec l'aligneur Python mesuré, sur 600 cas.
|
|
33
|
+
- Note simple, et note apprise `NOTE_APPRISE_1` : réseau 64-64-1 par son, ridge par phrase. Parité avec Python à 10⁻³ près. Seuils par défaut : 7,5, 1,5 et 1,5.
|
|
34
|
+
- `resistances(resultats, { minEssais })`.
|
|
35
|
+
- Adaptateurs `reconnaisseurFixe` et `reconnaisseurHttp` (contrat HTTP fixé).
|
|
36
|
+
- `ErreurPrononciation`, `CODES`, `LIBELLES` : huit codes nommés.
|
|
37
|
+
- Bornes : taille de l'audio et nombre de mots.
|
|
38
|
+
- Exemple de vérification `examples/verifier.mjs` : trois phrases de speechocean762.
|
|
39
|
+
|
|
40
|
+
### Points ouverts
|
|
41
|
+
- Le service Python de reconnaissance (wav2vec2, traits GOP) est prévu pour la 0.2.0.
|
|
42
|
+
- Les poids sont appris sur des voix chinoises : remesure sur des voix algériennes (lot 0, tâche 0.4).
|
|
43
|
+
- Les renvois `ne_fait_pas` vers `@mostajs/voix`, `prononciation-ui` et `consentement` visent des modules prévus, pas encore créés.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@mostajs/prononciation
|
|
2
|
+
Copyright (C) 2026 Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
|
|
4
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
5
|
+
the terms of the GNU Affero General Public License as published by the Free
|
|
6
|
+
Software Foundation, either version 3 of the License, or (at your option) any
|
|
7
|
+
later version.
|
|
8
|
+
|
|
9
|
+
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
10
|
+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
11
|
+
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
12
|
+
details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.html>.
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mostajs/prononciation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later · zéro dépendance · Node ≥ 18
|
|
4
|
+
|
|
5
|
+
Ce module dit si une voix a prononcé un texte connu, et où elle s'en écarte, son par son. Il fournit :
|
|
6
|
+
- une note par son, par mot et par phrase ;
|
|
7
|
+
- un verdict au seuil ;
|
|
8
|
+
- la liste des sons qui résistent dans une classe.
|
|
9
|
+
|
|
10
|
+
## Installer
|
|
11
|
+
|
|
12
|
+
```bash
|
|
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
|
|
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
|
+
|
|
19
|
+
## Exemple minimal
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { createPrononciation, lexiqueCmudict, combinerLexiques, creerLexique,
|
|
23
|
+
reconnaisseurHttp, NOTE_APPRISE_1, resistances } from '@mostajs/prononciation';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
|
|
26
|
+
const p = createPrononciation({
|
|
27
|
+
lexique: combinerLexiques(creerLexique({ BOUMERDES: 'b u m ɛ ɹ d ɛ s' }),
|
|
28
|
+
lexiqueCmudict(readFileSync('cmudict.dict', 'utf8'))),
|
|
29
|
+
reconnaisseur: reconnaisseurHttp({ url: 'http://127.0.0.1:8762/reconnaitre', type: 'audio/wav' }),
|
|
30
|
+
note: NOTE_APPRISE_1,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const r = await p.evaluer({ texte: 'I would like a cup of tea.', audio }); // audio : Uint8Array
|
|
34
|
+
r.verdict; // 'accepte' | 'a-reprendre'
|
|
35
|
+
r.mots.filter((m) => m.etat === 'a-reprendre'); // les mots à reprendre, avec leurs sons
|
|
36
|
+
resistances(resultatsDeLaClasse, { minEssais: 3 }); // [{ phoneme: 'θ', essais, echecs, taux }, …]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Sans `note`, le module emploie la **note simple**, c'est-à-dire la part des sons retrouvés. Le résultat dit toujours le `mode` utilisé.
|
|
40
|
+
|
|
41
|
+
## Ce qu'on injecte
|
|
42
|
+
|
|
43
|
+
| port | forme | fourni par |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `lexique` | `{ phonemes(mot) → string[] \| null }` | `creerLexique`, `lexiqueCmudict` (fichier BSD-2 fourni par l'application), `combinerLexiques` |
|
|
46
|
+
| `reconnaisseur` | `{ reconnaitre({ audio, attendus }) → { entendus, traits?, duree? } }` | `reconnaisseurHttp` (service de reconnaissance local), `reconnaisseurFixe` (essais) |
|
|
47
|
+
|
|
48
|
+
## Ce que vaut la note
|
|
49
|
+
|
|
50
|
+
Mesures sur la partie test de speechocean762, faite de 2 500 phrases et de locuteurs absents de l'apprentissage (Pearson avec la note des juges) :
|
|
51
|
+
|
|
52
|
+
| | note simple | note apprise | GOPT (ICASSP 2022) |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| phrase | 0,54 | **0,72** | 0,714 |
|
|
55
|
+
| mot | — | **0,54** | 0,533 |
|
|
56
|
+
| son | 0,35 | **0,56** | 0,612 |
|
|
57
|
+
|
|
58
|
+
Les poids sont appris sur des voix d'apprenants chinois. Il faut les remesurer sur la population servie.
|
|
59
|
+
|
|
60
|
+
## Vérifier
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm test # 23 essais (mjs-unit) + 9 essais du service (Python), parités comprises
|
|
64
|
+
node examples/verifier.mjs # trois vraies phrases, par le vrai adaptateur HTTP
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Attribution
|
|
68
|
+
|
|
69
|
+
Les poids `note-apprise-1` sont appris sur **speechocean762** (Zhang et al., 2021), sous licence CC BY 4.0 : https://www.openslr.org/101. Le modèle acoustique visé est `facebook/wav2vec2-lv-60-espeak-cv-ft` (Apache-2.0).
|
|
70
|
+
|
|
71
|
+
## Référence
|
|
72
|
+
|
|
73
|
+
L'API complète est dans `llms.txt`.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @mostajs/prononciation 0.1.0 — fiche LLM
|
|
2
|
+
# Auteur : Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later · N1 générique, forme moteur, zéro dépendance
|
|
3
|
+
|
|
4
|
+
RÔLE
|
|
5
|
+
Comparer une voix à un texte CONNU. Le module :
|
|
6
|
+
- tire du texte les sons attendus (lexique injecté) ;
|
|
7
|
+
- obtient du reconnaisseur injecté les sons entendus, et éventuellement les traits GOP ;
|
|
8
|
+
- aligne les deux suites ;
|
|
9
|
+
- note chaque son (0-2), chaque mot (0-2) et la phrase (0-10) ;
|
|
10
|
+
- rend un verdict au seuil ;
|
|
11
|
+
- agrège les sons qui résistent.
|
|
12
|
+
Le module ne garde rien : pas de dépôt, pas de table, pas d'audio.
|
|
13
|
+
|
|
14
|
+
QUESTION
|
|
15
|
+
Cette phrase a-t-elle été bien dite, et quel son faut-il reprendre ?
|
|
16
|
+
|
|
17
|
+
EXPORTS (src/index.js)
|
|
18
|
+
createPrononciation({ lexique, reconnaisseur, note?, seuils?, regle?, limites? }) → { evaluer, attendus, seuils, regle, mode }
|
|
19
|
+
evaluer({ texte, audio }) → Promise<Resultat> attendus(texte) → [{ mot, phonemes }]
|
|
20
|
+
creerLexique(table) table { MOT: 'k ʌ p' | 'K AH1 P' | ['k','ʌ','p'] } ; IPA ou ARPAbet détecté
|
|
21
|
+
lexiqueCmudict(texte) texte du fichier CMUdict (BSD-2), fourni par l'application ; 1re variante gardée
|
|
22
|
+
combinerLexiques(...lexiques) le premier qui connaît le mot l'emporte (mots de l'enseignant d'abord)
|
|
23
|
+
arpaVersIpa('AH0') → 'ʌ0' normaliserJetons(jetons espeak) → classes egal(attendu, entendu, regle)
|
|
24
|
+
motsDe(texte) → ['I\'D', 'LIKE', …]
|
|
25
|
+
aligner(attendus, entendus, { regle }) → { etats, entendus (en face, null si omis), insertions [{ apres, entendu }] }
|
|
26
|
+
appliquerNote(note, { phonemes, etats, traits, nEntendus, duree }) → { notes (brutes), note (brute) }
|
|
27
|
+
noteSimple(etats) validerNote(note)
|
|
28
|
+
resistances(resultats, { minEssais }) → [{ phoneme, essais, echecs, taux }] trié
|
|
29
|
+
reconnaisseurFixe(reponse | ({ audio, attendus }) => reponse) — essais
|
|
30
|
+
reconnaisseurHttp({ url, fetch?, delaiMs = 15000, type? }) — client du service de reconnaissance
|
|
31
|
+
NOTE_APPRISE_1 (aussi '@mostajs/prononciation/poids/note-apprise-1') ErreurPrononciation, CODES, LIBELLES
|
|
32
|
+
ARPA, PHONEMES (39 classes), REGLES ['stricte-2', 'tolerante-2']
|
|
33
|
+
|
|
34
|
+
TYPES
|
|
35
|
+
Resultat = { verdict: 'accepte'|'a-reprendre', note: 0-10, mode: 'apprise'|'simple', regle, seuils, poids?,
|
|
36
|
+
mots: [{ mot, note: 0-2, etat: 'juste'|'a-reprendre', insertions: string[],
|
|
37
|
+
phonemes: [{ attendu, entendu|null, note: 0-2, etat: 'juste'|'faux'|'omis', aReprendre }] }],
|
|
38
|
+
attendus, entendus, duree? }
|
|
39
|
+
Port lexique { phonemes(mot) → string[] | null } (sons IPA ; suffixe 0 = non accentué, AH0/IY0)
|
|
40
|
+
Port reconnaisseur { reconnaitre({ audio, attendus }) → Promise<{ entendus: string[], traits?: number[8][], duree?: s }> }
|
|
41
|
+
traits par son ATTENDU : gop_moy, gop_max, fenetre_max, lpr_moy, lpr_max, duree (trames), fenetre (trames), blanc_moy
|
|
42
|
+
Contrat HTTP : POST <url>, corps = octets de l'audio ; en-tête x-attendus = encodeURIComponent(JSON des sons attendus) ;
|
|
43
|
+
réponse 200 JSON { entendus, traits?, duree? }.
|
|
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
|
+
|
|
56
|
+
NOTES ET SEUILS
|
|
57
|
+
- Simple (aucune note configurée) : son = 2 si retrouvé, sinon 0 ; phrase = 10 × part retrouvée. Seuils : phrase 6, mot 1, son 1.
|
|
58
|
+
- Apprise (note: NOTE_APPRISE_1) :
|
|
59
|
+
- par son, traits + état glouton + identité du son → réseau 64-64-1 ;
|
|
60
|
+
- par phrase, 9 agrégats → ridge ;
|
|
61
|
+
- seuils : phrase 7,5 ; mot 1,5 ; son 1,5.
|
|
62
|
+
Accord avec les juges sur la partie test de speechocean762 : phrase 0,72, mot 0,54, son 0,56 (GOPT publié : 0,714, 0,533, 0,612).
|
|
63
|
+
- Un mot est « a-reprendre » si la moyenne de ses sons est sous seuils.mot ; un son est aReprendre sous seuils.phoneme.
|
|
64
|
+
|
|
65
|
+
ERREURS (R-I) — ErreurPrononciation { code, statut }
|
|
66
|
+
CONFIGURATION 400 · TEXTE_VIDE 400 · TEXTE_TROP_LONG 413 · MOT_INCONNU 422 (e.mots : tous les inconnus)
|
|
67
|
+
AUDIO_ABSENT 400 · AUDIO_TROP_LONG 413 · RECONNAISSEUR 502 · TRAITS 502
|
|
68
|
+
|
|
69
|
+
PIÈGES
|
|
70
|
+
- Note apprise sans traits ni durée → TRAITS. Il n'y a JAMAIS de repli silencieux sur la note simple : lire r.mode.
|
|
71
|
+
- Les poids note-apprise-1 sont appris sur des voix CHINOISES (speechocean762) : à remesurer sur la population servie.
|
|
72
|
+
- Les sons entendus doivent être des CLASSES (normaliserJetons pour espeak) ; un symbole hors classe compte faux.
|
|
73
|
+
- Bornes par défaut : 480 000 octets d'audio, 40 mots. Les fixer dans limites selon l'enregistreur.
|
|
74
|
+
- Le module n'efface ni ne stocke l'audio : c'est à l'application de le faire.
|
|
75
|
+
- Attribution : les poids dérivent de speechocean762 (CC BY 4.0, https://www.openslr.org/101). Citer le corpus.
|
|
76
|
+
|
|
77
|
+
NE FAIT PAS
|
|
78
|
+
faire entendre une phrase → @mostajs/voix · capter l'audio → @mostajs/media · ranger l'audio → @mostajs/storage
|
|
79
|
+
enregistrer une note → @mostajs/gradebook · écrans → @mostajs/prononciation-ui · consentement → @mostajs/consentement
|
package/package.json
CHANGED
|
@@ -1,6 +1,79 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/prononciation",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./poids/note-apprise-1": "./src/poids/note-apprise-1.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"service",
|
|
16
|
+
"!service/.venv",
|
|
17
|
+
"!service/**/__pycache__",
|
|
18
|
+
"llms.txt",
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "bash test-scripts/run-tests.sh"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"mostajs",
|
|
31
|
+
"prononciation",
|
|
32
|
+
"pronunciation",
|
|
33
|
+
"pronunciation-assessment",
|
|
34
|
+
"goodness-of-pronunciation",
|
|
35
|
+
"gop",
|
|
36
|
+
"phoneme",
|
|
37
|
+
"speech",
|
|
38
|
+
"english",
|
|
39
|
+
"cmudict",
|
|
40
|
+
"wav2vec2",
|
|
41
|
+
"speech-recognition"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@mostajs/mjs-unit": "0.10.0"
|
|
46
|
+
},
|
|
47
|
+
"mostajs": {
|
|
48
|
+
"niveau": "N1",
|
|
49
|
+
"nature": "generique",
|
|
50
|
+
"reutilisabilite": "transverse",
|
|
51
|
+
"depend_de": [],
|
|
52
|
+
"question": "Cette phrase a-t-elle été bien dite, et quel son faut-il reprendre ?",
|
|
53
|
+
"forme": "moteur",
|
|
54
|
+
"entites": [],
|
|
55
|
+
"verbes": [
|
|
56
|
+
"evaluer",
|
|
57
|
+
"attendus",
|
|
58
|
+
"aligner",
|
|
59
|
+
"resistances",
|
|
60
|
+
"creerLexique",
|
|
61
|
+
"lexiqueCmudict",
|
|
62
|
+
"reconnaisseurHttp"
|
|
63
|
+
],
|
|
64
|
+
"interop": [
|
|
65
|
+
"IPA (Alphabet phonétique international)",
|
|
66
|
+
"ARPAbet / CMUdict",
|
|
67
|
+
"speechocean762 (échelles de notes 0-2 par son, 0-10 par phrase)",
|
|
68
|
+
"HTTP : POST /reconnaitre (x-attendus), GET /sante — service/README.md"
|
|
69
|
+
],
|
|
70
|
+
"ne_fait_pas": {
|
|
71
|
+
"faire entendre une phrase": "@mostajs/voix",
|
|
72
|
+
"capter l'audio dans le navigateur": "@mostajs/media",
|
|
73
|
+
"ranger l'audio": "@mostajs/storage",
|
|
74
|
+
"enregistrer une note": "@mostajs/gradebook",
|
|
75
|
+
"rendre les écrans": "@mostajs/prononciation-ui",
|
|
76
|
+
"recueillir un consentement": "@mostajs/consentement"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
L'installeur fait trois choses :
|
|
16
|
+
- il crée `service/.venv` et y installe les dépendances **figées** (`requirements.txt` : torch 2.4.1 CPU, transformers 4.44.2…) ;
|
|
17
|
+
- il télécharge `facebook/wav2vec2-lv-60-espeak-cv-ft` à sa **révision figée** (`ae45363b`) ;
|
|
18
|
+
- il vérifie que le modèle se charge sans réseau.
|
|
19
|
+
|
|
20
|
+
Une autre version des dépendances ou du modèle donne d'autres traits, et la note apprise ne vaut plus ses 0,72.
|
|
21
|
+
|
|
22
|
+
## Lancer
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
HF_HOME=/srv/modeles service/.venv/bin/python service/serveur.py --local-seulement
|
|
26
|
+
# écoute http://127.0.0.1:8762
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| option | défaut | rôle |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `--hote` | `127.0.0.1` (`PRONONCIATION_HOTE`) | écoute locale : seule l'application le joint |
|
|
32
|
+
| `--port` | `8762` (`PRONONCIATION_PORT`) | `0` : un port libre, annoncé sur la sortie |
|
|
33
|
+
| `--fils` | `2` (`PRONONCIATION_FILS`) | fils de calcul du modèle |
|
|
34
|
+
| `--octets` | `2000000` | taille maximale du corps |
|
|
35
|
+
| `--secondes` | `15` | durée maximale de l'audio |
|
|
36
|
+
| `--local-seulement` | non | ne rien télécharger ; sortie 3 si le modèle manque |
|
|
37
|
+
|
|
38
|
+
Côté application :
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
reconnaisseurHttp({ url: 'http://127.0.0.1:8762/reconnaitre', type: 'audio/wav' })
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Le contrat
|
|
45
|
+
|
|
46
|
+
- `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 }`.
|
|
47
|
+
- `GET /sante` : `{ ok, modele, revision, regle, classes }`.
|
|
48
|
+
- Refus `{ erreur, message }` :
|
|
49
|
+
|
|
50
|
+
| code | statut | quand |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `ATTENDUS` | 400 | `x-attendus` absent, illisible, vide, plus de 400 sons, ou un son de plus de 4 signes |
|
|
53
|
+
| `AUDIO_ABSENT` | 400 | corps vide, ou moins de 0,25 s |
|
|
54
|
+
| `LONGUEUR` | 411 | pas de `content-length` |
|
|
55
|
+
| `TROP_GROS` | 413 | corps au-delà de `--octets` (refusé sans être lu) |
|
|
56
|
+
| `TROP_LONG` | 413 | audio au-delà de `--secondes` |
|
|
57
|
+
| `FORMAT` | 415 | audio illisible (le WebM/Opus des navigateurs ne l'est pas : enregistrer en WAV) |
|
|
58
|
+
| `CHEMIN`, `METHODE` | 404, 405 | autre adresse, autre méthode |
|
|
59
|
+
| `INTERNE` | 500 | toute autre faute |
|
|
60
|
+
|
|
61
|
+
## Ce qu'il ne garde pas
|
|
62
|
+
|
|
63
|
+
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).
|
|
64
|
+
|
|
65
|
+
## Débit
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
## Essais
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
PRONONCIATION_PYTHON=service/.venv/bin/python npm test # 23 essais JS + 9 essais du service
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
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,29 @@
|
|
|
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
|
+
#
|
|
9
|
+
# 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,
|
|
10
|
+
# puis vérifie qu'il se charge sans réseau. Rien d'autre n'est écrit.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
ICI="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
PY="${PYTHON:-python3}"
|
|
14
|
+
"$PY" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)' || { echo "✗ Python ≥ 3.10 requis ($PY)"; exit 1; }
|
|
15
|
+
[ -x "$ICI/.venv/bin/python" ] || "$PY" -m venv "$ICI/.venv"
|
|
16
|
+
"$ICI/.venv/bin/pip" install -q --upgrade pip
|
|
17
|
+
"$ICI/.venv/bin/pip" install -q -r "$ICI/requirements.txt"
|
|
18
|
+
[ -n "${PRONONCIATION_HF_HOME:-}" ] && export HF_HOME="$PRONONCIATION_HF_HOME"
|
|
19
|
+
cd "$ICI"
|
|
20
|
+
.venv/bin/python - <<'PY'
|
|
21
|
+
from huggingface_hub import hf_hub_download
|
|
22
|
+
import moteur
|
|
23
|
+
for f in moteur.FICHIERS:
|
|
24
|
+
hf_hub_download(moteur.MODELE, f, revision=moteur.REVISION)
|
|
25
|
+
assert moteur.modele_installe(), 'modèle absent après téléchargement'
|
|
26
|
+
m = moteur.Moteur(local_seulement=True)
|
|
27
|
+
print(f'✓ modèle {moteur.MODELE}@{moteur.REVISION[:8]} chargé : {m.classes} classes de sons, règle {m.regle}')
|
|
28
|
+
PY
|
|
29
|
+
echo "✓ installé. Lancer : ${PRONONCIATION_HF_HOME:+HF_HOME=$PRONONCIATION_HF_HOME }$ICI/.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
|