@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 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
- # Temporary Holding Version
1
+ # @mostajs/prononciation
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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.
@@ -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