openpronounce 0.2.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Jean-François Lépine <http://blog.lepine.pro>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.4
2
+ Name: openpronounce
3
+ Version: 0.2.0
4
+ Summary: Open-source, phoneme-level English pronunciation assessment (Wav2Vec2 + DTW). Score a recording, list mispronounced words, get prosody curves.
5
+ Author: Jean-François Lépine
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Halleck45/OpenPronounce
8
+ Project-URL: Repository, https://github.com/Halleck45/OpenPronounce
9
+ Project-URL: Issues, https://github.com/Halleck45/OpenPronounce/issues
10
+ Project-URL: Blog post, https://blog.lepine.pro/en/ai-wav2vec-pronunciation-vectorization/
11
+ Keywords: pronunciation,pronunciation-assessment,speech,phonemes,wav2vec2,CAPT,language-learning,asr,prosody,visemes
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Education
21
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: torch>=2.0
27
+ Requires-Dist: transformers>=4.30
28
+ Requires-Dist: librosa>=0.10
29
+ Requires-Dist: soundfile>=0.12
30
+ Requires-Dist: numpy>=1.24
31
+ Requires-Dist: scipy>=1.10
32
+ Requires-Dist: scikit-learn>=1.2
33
+ Requires-Dist: fastdtw>=0.3
34
+ Requires-Dist: phonemizer>=3.2
35
+ Requires-Dist: Levenshtein>=0.20
36
+ Requires-Dist: gTTS>=2.3
37
+ Provides-Extra: app
38
+ Requires-Dist: fastapi>=0.100; extra == "app"
39
+ Requires-Dist: uvicorn[standard]>=0.23; extra == "app"
40
+ Requires-Dist: python-multipart>=0.0.6; extra == "app"
41
+ Requires-Dist: jinja2>=3.1; extra == "app"
42
+ Requires-Dist: streamlit>=1.30; extra == "app"
43
+ Provides-Extra: dev
44
+ Requires-Dist: pytest>=7; extra == "dev"
45
+ Requires-Dist: pytest-cov>=4; extra == "dev"
46
+ Requires-Dist: httpx>=0.24; extra == "dev"
47
+ Dynamic: license-file
48
+
49
+ <h1 align="center">OpenPronounce</h1>
50
+
51
+ <p align="center">
52
+ <b>Open-source, phoneme-level English pronunciation assessment.</b><br>
53
+ Give it a recording and the sentence that was supposed to be said. Get a score, the mispronounced words with expected vs. heard phonemes (IPA), the transcription and the prosody curves. Runs on your machine, on CPU.
54
+ </p>
55
+
56
+ <p align="center">
57
+ <a href="https://colab.research.google.com/github/Halleck45/OpenPronounce/blob/main/OpenPronounce-demo.ipynb"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open in Colab"></a>
58
+ <a href="https://github.com/Halleck45/OpenPronounce/actions/workflows/tests.yml"><img src="https://github.com/Halleck45/OpenPronounce/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
59
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
60
+ <a href="https://github.com/sponsors/Halleck45"><img src="https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86" alt="Sponsor"></a>
61
+ </p>
62
+
63
+ <p align="center">
64
+ <img src="./docs/open-pronounce-preview.png" alt="OpenPronounce web application: score, mispronounced words, phoneme and prosody charts" width="720">
65
+ </p>
66
+
67
+ ```console
68
+ $ openpronounce recording.wav "Hello, how are you?"
69
+ Score : 55.74/100
70
+ Transcription: HELL NO WHO ARE YOU
71
+ Heard phones : /h ɛ l n oʊ h u ɑɹ j u/
72
+ Mispronounced:
73
+ - hello: expected /həloʊ/, heard /hɛlnoʊ/
74
+ - how: expected /haʊ/, heard /hu/
75
+ ```
76
+
77
+ Commercial APIs (Azure Speech *Pronunciation Assessment*, SpeechAce, ELSA...) do this behind a paywall and a network call. OpenPronounce is the self-hosted, MIT-licensed building block for language-learning apps, EdTech products and research: no API key, no per-minute billing, your learners' voices stay on your servers.
78
+
79
+ ## What you get
80
+
81
+ For each recording, a JSON-serializable dict:
82
+
83
+ | Field | Meaning |
84
+ |---|---|
85
+ | `score` | 0-100 overall pronunciation score |
86
+ | `transcribe` | what the model actually heard (Wav2Vec2 CTC) |
87
+ | `differences.errors[]` | one entry per mispronounced or missing word: `word`, `expected` (IPA), `actual` (IPA, what was really heard), `position` |
88
+ | `differences.heard_phones`, `differences.expected_phones` | the phones recognized in the audio, and the phones expected for each word |
89
+ | `differences.words_with_errors` | the words to work on |
90
+ | `differences.phoneme_error_rate`, `differences.word_error_rate` | edited phonemes / expected phonemes, edited words / expected words |
91
+ | `differences.expected_phonemes`, `differences.transcribed_phonemes` | full phoneme sequences |
92
+ | `differences.expected_vector`, `differences.transcribed_vector` | DTW-aligned phoneme traces, ready to plot |
93
+ | `acoustic_distance` | mean per-frame DTW distance between the learner's Wav2Vec2 embeddings and a synthetic reference |
94
+ | `prosody.f0`, `prosody.energy` | pitch and loudness contours |
95
+
96
+ ## Quickstart
97
+
98
+ ### Install
99
+
100
+ Requires Python 3.10+, `ffmpeg` and `espeak-ng` on the system (`apt install ffmpeg espeak-ng`, `brew install ffmpeg espeak-ng`).
101
+
102
+ ```bash
103
+ pip install torch --index-url https://download.pytorch.org/whl/cpu # CPU wheels, much smaller
104
+ pip install git+https://github.com/Halleck45/OpenPronounce.git
105
+ ```
106
+
107
+ Two Wav2Vec2 checkpoints (~1.2 GB each) are downloaded from the Hugging Face Hub on first use: `facebook/wav2vec2-large-960h` (words) and `facebook/wav2vec2-lv-60-espeak-cv-ft` (phones). Set `OPENPRONOUNCE_PHONEME_MODEL=off` to skip the second one; word errors are then inferred from the transcription, which is less precise.
108
+
109
+ ### Command line
110
+
111
+ ```bash
112
+ openpronounce recording.wav "Hello, I am a developer"
113
+ openpronounce recording.mp3 "Hello, I am a developer" --json --no-prosody # machine-readable
114
+ ```
115
+
116
+ ### Python
117
+
118
+ ```python
119
+ from openpronounce import load_audio, compare_audio_with_text
120
+
121
+ sound = load_audio("recording.wav") # any format ffmpeg reads, resampled to 16 kHz mono
122
+ result = compare_audio_with_text(sound, "Hello, I am a developer")
123
+
124
+ print(result["score"]) # 97.36
125
+ for err in result["differences"]["errors"]:
126
+ print(err["word"], err["expected"], "->", err["actual"] or "(missing)")
127
+ ```
128
+
129
+ Lower-level building blocks are exposed too: `transcribe(sound)`, `get_phonemes(text)`, `compare_transcriptions(heard_text, expected_text)`.
130
+
131
+ ### Docker
132
+
133
+ ```bash
134
+ docker build -t openpronounce .
135
+ docker run -p 8000:8000 openpronounce
136
+ # open http://localhost:8000
137
+ ```
138
+
139
+ ### Web application (FastAPI)
140
+
141
+ ```bash
142
+ pip install "openpronounce[app] @ git+https://github.com/Halleck45/OpenPronounce.git"
143
+ git clone https://github.com/Halleck45/OpenPronounce.git && cd OpenPronounce
144
+ uvicorn server:app --host 0.0.0.0 --port 8000
145
+ ```
146
+
147
+ The UI records from the microphone, scores the sentence, animates a mouth (visemes) and plots the phoneme traces and prosody. Browsers only allow microphone access on `https://` or `localhost`.
148
+
149
+ | Endpoint | Body (multipart form) | Returns |
150
+ |---|---|---|
151
+ | `POST /pronunciation` | `file`, `expected_text` | full analysis (see above) |
152
+ | `POST /speech2text` | `file` | `{"transcript": ...}` |
153
+ | `POST /phonemes` | `text` | `{"phonemes": [...], "words": [...]}` |
154
+ | `POST /tts` | `text` | reference pronunciation, 16 kHz wav |
155
+ | `GET /health` | | `{"status": "ok"}` |
156
+
157
+ Interactive docs at `/docs` (Swagger UI).
158
+
159
+ ### Streamlit
160
+
161
+ ```bash
162
+ streamlit run streamlit_app.py
163
+ ```
164
+
165
+ ### Notebook
166
+
167
+ [Open in Colab](https://colab.research.google.com/github/Halleck45/OpenPronounce/blob/main/OpenPronounce-demo.ipynb): load a sample, score it, print the phoneme errors, plot the prosody, then try your own recording. No local setup.
168
+
169
+ ## How it works
170
+
171
+ 1. **Phones**: a Wav2Vec2 model fine-tuned on espeak labels (`wav2vec2-lv-60-espeak-cv-ft`) recognizes the phones actually said, straight from the audio. No word-level language model gets a chance to "correct" the learner.
172
+ 2. **Expected phones**: the sentence is phonemized with espeak-ng (IPA), word by word. Both sequences are normalized (length marks dropped, reduced vowels merged, cot-caught merger, a few function words with alternate pronunciations).
173
+ 3. **Alignment**: expected and heard phones are aligned with edit-distance opcodes; each word is compared with the phones it aligned to and reported when half of them (or 3 or more) are wrong. Thresholds: `phones.PHONE_ERROR_THRESHOLD`, `phones.PHONE_ERROR_MIN_EDITS`.
174
+ 4. **Words**: the audio is also transcribed with `wav2vec2-large-960h` for the transcription and the word error rate.
175
+ 5. **Acoustics**: the sentence is synthesized (gTTS), both recordings are encoded with Wav2Vec2 and aligned with DTW; the mean per-frame distance is the `acoustic_distance`.
176
+ 6. **Prosody**: F0 (pYIN) and RMS energy contours.
177
+
178
+ The approach is described in [this blog post](https://blog.lepine.pro/en/ai-wav2vec-pronunciation-vectorization/).
179
+
180
+ ### The score
181
+
182
+ `score = 0.2 × acoustic + 0.5 × (1 − phoneme error rate) + 0.3 × (1 − word error rate)`, each term clipped to [0, 100].
183
+ The acoustic term maps the mean DTW distance linearly from 5 (100) to 15 (0); these bounds come from the bundled samples (`assets/`) and are exposed as `speech.ACOUSTIC_DISTANCE_GOOD` / `speech.ACOUSTIC_DISTANCE_BAD` if you want to recalibrate on your own data. All three terms are length-independent, so a long paragraph and a two-word sentence are scored on the same scale.
184
+
185
+ ## Visemes
186
+
187
+ The web UI ships a small [phoneme-to-viseme](static/viseme.js) mapping for English (HumanBeanCMU39 mouth shapes), enough to animate a talking mouth from the phoneme list:
188
+
189
+ ```javascript
190
+ import { Viseme } from "/static/viseme.js";
191
+ const viseme = new Viseme(document.getElementById("mouth"));
192
+ viseme.play(["həloʊ", "huː", "ɑːɹ", "juː"]);
193
+ ```
194
+
195
+ ## Limitations
196
+
197
+ - English only for now (`en-us` phonemization, English Wav2Vec2). Swapping the model and the espeak language is the path to other languages.
198
+ - The reference voice comes from gTTS, so the first analysis of a given sentence needs network access; references are cached afterwards.
199
+ - Wav2Vec2 was trained on native read speech (LibriSpeech). Very strong accents, children's voices and noisy recordings degrade the transcription, and therefore the feedback.
200
+ - The phone recognizer itself has an error rate (about 10 % of phones on a clean native reading of the bundled Harvard sentences); expect an occasional false alarm on short words. This is a heuristic assessment, not a Goodness-of-Pronunciation model trained on annotated L2 speech.
201
+
202
+ ## Roadmap
203
+
204
+ Contributions welcome on any of these:
205
+
206
+ - [ ] Publish on PyPI (`pip install openpronounce`)
207
+ - [ ] Hosted demo (Hugging Face Space)
208
+ - [ ] Offline TTS reference (piper / Kokoro) instead of gTTS
209
+ - [ ] Per-phone confidence (CTC posteriors) to grade errors instead of a yes/no per word
210
+ - [ ] Other languages
211
+ - [ ] Benchmark on a public L2 dataset (speechocean762) to calibrate the score
212
+ - [ ] GPU support in the Docker image
213
+
214
+ ## Contributing
215
+
216
+ ```bash
217
+ git clone https://github.com/Halleck45/OpenPronounce.git && cd OpenPronounce
218
+ python -m venv .venv && source .venv/bin/activate
219
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
220
+ pip install -e ".[app,dev]"
221
+ pytest
222
+ ```
223
+
224
+ Tests do not need the network nor the model weights (model calls are mocked); espeak-ng must be installed.
225
+
226
+ ## References
227
+
228
+ - [Vectorisation of sounds for pronunciation](https://blog.lepine.pro/en/ai-wav2vec-pronunciation-vectorization/) (blog post about this project)
229
+ - [wav2vec 2.0](https://ai.meta.com/research/impact/wav2vec/), Baevski et al., 2020
230
+ - [Azure Speech visemes](https://learn.microsoft.com/azure/ai-services/speech-service/how-to-speech-synthesis-viseme) and [SSML phonetic sets](https://learn.microsoft.com/azure/ai-services/speech-service/speech-ssml-phonetic-sets)
231
+ - Mouth images: HumanBeanCMU39 viseme set
232
+
233
+ ## License
234
+
235
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,187 @@
1
+ <h1 align="center">OpenPronounce</h1>
2
+
3
+ <p align="center">
4
+ <b>Open-source, phoneme-level English pronunciation assessment.</b><br>
5
+ Give it a recording and the sentence that was supposed to be said. Get a score, the mispronounced words with expected vs. heard phonemes (IPA), the transcription and the prosody curves. Runs on your machine, on CPU.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://colab.research.google.com/github/Halleck45/OpenPronounce/blob/main/OpenPronounce-demo.ipynb"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open in Colab"></a>
10
+ <a href="https://github.com/Halleck45/OpenPronounce/actions/workflows/tests.yml"><img src="https://github.com/Halleck45/OpenPronounce/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
11
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
12
+ <a href="https://github.com/sponsors/Halleck45"><img src="https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86" alt="Sponsor"></a>
13
+ </p>
14
+
15
+ <p align="center">
16
+ <img src="./docs/open-pronounce-preview.png" alt="OpenPronounce web application: score, mispronounced words, phoneme and prosody charts" width="720">
17
+ </p>
18
+
19
+ ```console
20
+ $ openpronounce recording.wav "Hello, how are you?"
21
+ Score : 55.74/100
22
+ Transcription: HELL NO WHO ARE YOU
23
+ Heard phones : /h ɛ l n oʊ h u ɑɹ j u/
24
+ Mispronounced:
25
+ - hello: expected /həloʊ/, heard /hɛlnoʊ/
26
+ - how: expected /haʊ/, heard /hu/
27
+ ```
28
+
29
+ Commercial APIs (Azure Speech *Pronunciation Assessment*, SpeechAce, ELSA...) do this behind a paywall and a network call. OpenPronounce is the self-hosted, MIT-licensed building block for language-learning apps, EdTech products and research: no API key, no per-minute billing, your learners' voices stay on your servers.
30
+
31
+ ## What you get
32
+
33
+ For each recording, a JSON-serializable dict:
34
+
35
+ | Field | Meaning |
36
+ |---|---|
37
+ | `score` | 0-100 overall pronunciation score |
38
+ | `transcribe` | what the model actually heard (Wav2Vec2 CTC) |
39
+ | `differences.errors[]` | one entry per mispronounced or missing word: `word`, `expected` (IPA), `actual` (IPA, what was really heard), `position` |
40
+ | `differences.heard_phones`, `differences.expected_phones` | the phones recognized in the audio, and the phones expected for each word |
41
+ | `differences.words_with_errors` | the words to work on |
42
+ | `differences.phoneme_error_rate`, `differences.word_error_rate` | edited phonemes / expected phonemes, edited words / expected words |
43
+ | `differences.expected_phonemes`, `differences.transcribed_phonemes` | full phoneme sequences |
44
+ | `differences.expected_vector`, `differences.transcribed_vector` | DTW-aligned phoneme traces, ready to plot |
45
+ | `acoustic_distance` | mean per-frame DTW distance between the learner's Wav2Vec2 embeddings and a synthetic reference |
46
+ | `prosody.f0`, `prosody.energy` | pitch and loudness contours |
47
+
48
+ ## Quickstart
49
+
50
+ ### Install
51
+
52
+ Requires Python 3.10+, `ffmpeg` and `espeak-ng` on the system (`apt install ffmpeg espeak-ng`, `brew install ffmpeg espeak-ng`).
53
+
54
+ ```bash
55
+ pip install torch --index-url https://download.pytorch.org/whl/cpu # CPU wheels, much smaller
56
+ pip install git+https://github.com/Halleck45/OpenPronounce.git
57
+ ```
58
+
59
+ Two Wav2Vec2 checkpoints (~1.2 GB each) are downloaded from the Hugging Face Hub on first use: `facebook/wav2vec2-large-960h` (words) and `facebook/wav2vec2-lv-60-espeak-cv-ft` (phones). Set `OPENPRONOUNCE_PHONEME_MODEL=off` to skip the second one; word errors are then inferred from the transcription, which is less precise.
60
+
61
+ ### Command line
62
+
63
+ ```bash
64
+ openpronounce recording.wav "Hello, I am a developer"
65
+ openpronounce recording.mp3 "Hello, I am a developer" --json --no-prosody # machine-readable
66
+ ```
67
+
68
+ ### Python
69
+
70
+ ```python
71
+ from openpronounce import load_audio, compare_audio_with_text
72
+
73
+ sound = load_audio("recording.wav") # any format ffmpeg reads, resampled to 16 kHz mono
74
+ result = compare_audio_with_text(sound, "Hello, I am a developer")
75
+
76
+ print(result["score"]) # 97.36
77
+ for err in result["differences"]["errors"]:
78
+ print(err["word"], err["expected"], "->", err["actual"] or "(missing)")
79
+ ```
80
+
81
+ Lower-level building blocks are exposed too: `transcribe(sound)`, `get_phonemes(text)`, `compare_transcriptions(heard_text, expected_text)`.
82
+
83
+ ### Docker
84
+
85
+ ```bash
86
+ docker build -t openpronounce .
87
+ docker run -p 8000:8000 openpronounce
88
+ # open http://localhost:8000
89
+ ```
90
+
91
+ ### Web application (FastAPI)
92
+
93
+ ```bash
94
+ pip install "openpronounce[app] @ git+https://github.com/Halleck45/OpenPronounce.git"
95
+ git clone https://github.com/Halleck45/OpenPronounce.git && cd OpenPronounce
96
+ uvicorn server:app --host 0.0.0.0 --port 8000
97
+ ```
98
+
99
+ The UI records from the microphone, scores the sentence, animates a mouth (visemes) and plots the phoneme traces and prosody. Browsers only allow microphone access on `https://` or `localhost`.
100
+
101
+ | Endpoint | Body (multipart form) | Returns |
102
+ |---|---|---|
103
+ | `POST /pronunciation` | `file`, `expected_text` | full analysis (see above) |
104
+ | `POST /speech2text` | `file` | `{"transcript": ...}` |
105
+ | `POST /phonemes` | `text` | `{"phonemes": [...], "words": [...]}` |
106
+ | `POST /tts` | `text` | reference pronunciation, 16 kHz wav |
107
+ | `GET /health` | | `{"status": "ok"}` |
108
+
109
+ Interactive docs at `/docs` (Swagger UI).
110
+
111
+ ### Streamlit
112
+
113
+ ```bash
114
+ streamlit run streamlit_app.py
115
+ ```
116
+
117
+ ### Notebook
118
+
119
+ [Open in Colab](https://colab.research.google.com/github/Halleck45/OpenPronounce/blob/main/OpenPronounce-demo.ipynb): load a sample, score it, print the phoneme errors, plot the prosody, then try your own recording. No local setup.
120
+
121
+ ## How it works
122
+
123
+ 1. **Phones**: a Wav2Vec2 model fine-tuned on espeak labels (`wav2vec2-lv-60-espeak-cv-ft`) recognizes the phones actually said, straight from the audio. No word-level language model gets a chance to "correct" the learner.
124
+ 2. **Expected phones**: the sentence is phonemized with espeak-ng (IPA), word by word. Both sequences are normalized (length marks dropped, reduced vowels merged, cot-caught merger, a few function words with alternate pronunciations).
125
+ 3. **Alignment**: expected and heard phones are aligned with edit-distance opcodes; each word is compared with the phones it aligned to and reported when half of them (or 3 or more) are wrong. Thresholds: `phones.PHONE_ERROR_THRESHOLD`, `phones.PHONE_ERROR_MIN_EDITS`.
126
+ 4. **Words**: the audio is also transcribed with `wav2vec2-large-960h` for the transcription and the word error rate.
127
+ 5. **Acoustics**: the sentence is synthesized (gTTS), both recordings are encoded with Wav2Vec2 and aligned with DTW; the mean per-frame distance is the `acoustic_distance`.
128
+ 6. **Prosody**: F0 (pYIN) and RMS energy contours.
129
+
130
+ The approach is described in [this blog post](https://blog.lepine.pro/en/ai-wav2vec-pronunciation-vectorization/).
131
+
132
+ ### The score
133
+
134
+ `score = 0.2 × acoustic + 0.5 × (1 − phoneme error rate) + 0.3 × (1 − word error rate)`, each term clipped to [0, 100].
135
+ The acoustic term maps the mean DTW distance linearly from 5 (100) to 15 (0); these bounds come from the bundled samples (`assets/`) and are exposed as `speech.ACOUSTIC_DISTANCE_GOOD` / `speech.ACOUSTIC_DISTANCE_BAD` if you want to recalibrate on your own data. All three terms are length-independent, so a long paragraph and a two-word sentence are scored on the same scale.
136
+
137
+ ## Visemes
138
+
139
+ The web UI ships a small [phoneme-to-viseme](static/viseme.js) mapping for English (HumanBeanCMU39 mouth shapes), enough to animate a talking mouth from the phoneme list:
140
+
141
+ ```javascript
142
+ import { Viseme } from "/static/viseme.js";
143
+ const viseme = new Viseme(document.getElementById("mouth"));
144
+ viseme.play(["həloʊ", "huː", "ɑːɹ", "juː"]);
145
+ ```
146
+
147
+ ## Limitations
148
+
149
+ - English only for now (`en-us` phonemization, English Wav2Vec2). Swapping the model and the espeak language is the path to other languages.
150
+ - The reference voice comes from gTTS, so the first analysis of a given sentence needs network access; references are cached afterwards.
151
+ - Wav2Vec2 was trained on native read speech (LibriSpeech). Very strong accents, children's voices and noisy recordings degrade the transcription, and therefore the feedback.
152
+ - The phone recognizer itself has an error rate (about 10 % of phones on a clean native reading of the bundled Harvard sentences); expect an occasional false alarm on short words. This is a heuristic assessment, not a Goodness-of-Pronunciation model trained on annotated L2 speech.
153
+
154
+ ## Roadmap
155
+
156
+ Contributions welcome on any of these:
157
+
158
+ - [ ] Publish on PyPI (`pip install openpronounce`)
159
+ - [ ] Hosted demo (Hugging Face Space)
160
+ - [ ] Offline TTS reference (piper / Kokoro) instead of gTTS
161
+ - [ ] Per-phone confidence (CTC posteriors) to grade errors instead of a yes/no per word
162
+ - [ ] Other languages
163
+ - [ ] Benchmark on a public L2 dataset (speechocean762) to calibrate the score
164
+ - [ ] GPU support in the Docker image
165
+
166
+ ## Contributing
167
+
168
+ ```bash
169
+ git clone https://github.com/Halleck45/OpenPronounce.git && cd OpenPronounce
170
+ python -m venv .venv && source .venv/bin/activate
171
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
172
+ pip install -e ".[app,dev]"
173
+ pytest
174
+ ```
175
+
176
+ Tests do not need the network nor the model weights (model calls are mocked); espeak-ng must be installed.
177
+
178
+ ## References
179
+
180
+ - [Vectorisation of sounds for pronunciation](https://blog.lepine.pro/en/ai-wav2vec-pronunciation-vectorization/) (blog post about this project)
181
+ - [wav2vec 2.0](https://ai.meta.com/research/impact/wav2vec/), Baevski et al., 2020
182
+ - [Azure Speech visemes](https://learn.microsoft.com/azure/ai-services/speech-service/how-to-speech-synthesis-viseme) and [SSML phonetic sets](https://learn.microsoft.com/azure/ai-services/speech-service/speech-ssml-phonetic-sets)
183
+ - Mouth images: HumanBeanCMU39 viseme set
184
+
185
+ ## License
186
+
187
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,35 @@
1
+ """OpenPronounce: open-source, phoneme-level English pronunciation assessment.
2
+
3
+ Typical usage::
4
+
5
+ from openpronounce import load_audio, compare_audio_with_text
6
+
7
+ sound = load_audio("recording.wav")
8
+ result = compare_audio_with_text(sound, "Hello, I am a developer")
9
+ print(result["score"], result["differences"]["errors"])
10
+ """
11
+
12
+ from .audio import load as load_audio, text2speech
13
+ from .phones import compare_phones, transcribe_phones
14
+ from .speech import (
15
+ compare_audio_with_text,
16
+ compare_transcriptions,
17
+ get_phonemes,
18
+ get_phonemes_with_word_mapping,
19
+ transcribe,
20
+ )
21
+
22
+ __version__ = "0.2.0"
23
+
24
+ __all__ = [
25
+ "__version__",
26
+ "load_audio",
27
+ "text2speech",
28
+ "compare_audio_with_text",
29
+ "compare_transcriptions",
30
+ "compare_phones",
31
+ "transcribe_phones",
32
+ "get_phonemes",
33
+ "get_phonemes_with_word_mapping",
34
+ "transcribe",
35
+ ]
@@ -0,0 +1,75 @@
1
+ """Audio loading, conversion and reference-speech generation."""
2
+
3
+ import hashlib
4
+ import logging
5
+ import os
6
+ import tempfile
7
+
8
+ import librosa
9
+ import soundfile as sf
10
+ from gtts import gTTS
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ TARGET_SR = 16000
15
+
16
+ CACHE_DIR = os.environ.get(
17
+ "OPENPRONOUNCE_CACHE_DIR",
18
+ os.path.join(tempfile.gettempdir(), "openpronounce"),
19
+ )
20
+
21
+
22
+ def load(file_path, sr=TARGET_SR):
23
+ """Load any audio file (wav, mp3, flac, ogg, webm, m4a...) as a mono float32 waveform at 16 kHz."""
24
+ waveform, _ = librosa.load(file_path, sr=sr, mono=True)
25
+ return waveform
26
+
27
+
28
+ def webm2wav(file_path):
29
+ """Convert a browser-recorded file (webm/ogg/wav/...) to a 16 kHz mono ``*.16k.wav`` file next to it.
30
+
31
+ Uses librosa (soundfile, then ffmpeg through audioread) so that ffmpeg is only
32
+ needed for formats libsndfile cannot read.
33
+ """
34
+ output_path = os.path.splitext(file_path)[0] + ".16k.wav"
35
+ try:
36
+ waveform = load(file_path)
37
+ except Exception as e: # noqa: BLE001
38
+ raise RuntimeError(
39
+ f"Unable to decode {file_path!r} ({e}). Make sure ffmpeg is installed."
40
+ ) from e
41
+ sf.write(output_path, waveform, TARGET_SR)
42
+ return output_path
43
+
44
+
45
+ # Backward-compatible alias (the original name was a typo)
46
+ webp2wav = webm2wav
47
+
48
+
49
+ def text2speech(text, lang="en", filename=None, target_sr=TARGET_SR):
50
+ """Generate a reference pronunciation of ``text`` as a 16 kHz mono wav file and return its path.
51
+
52
+ Uses gTTS (Google Translate TTS), so an internet connection is required the first
53
+ time a given text is requested. Results are cached in ``CACHE_DIR`` keyed by
54
+ ``(lang, text)`` so that repeated comparisons against the same sentence are free.
55
+ """
56
+ if filename is None:
57
+ os.makedirs(CACHE_DIR, exist_ok=True)
58
+ key = hashlib.sha1(f"{lang}\x00{target_sr}\x00{text}".encode("utf-8")).hexdigest()
59
+ filename = os.path.join(CACHE_DIR, f"tts-{key}.wav")
60
+ if os.path.exists(filename):
61
+ return filename
62
+
63
+ fd, mp3_path = tempfile.mkstemp(suffix=".mp3", prefix="openpronounce-tts-")
64
+ os.close(fd)
65
+ try:
66
+ gTTS(text=text, lang=lang, slow=False).save(mp3_path)
67
+ waveform = load(mp3_path, sr=target_sr)
68
+ finally:
69
+ try:
70
+ os.remove(mp3_path)
71
+ except OSError:
72
+ pass
73
+
74
+ sf.write(filename, waveform, target_sr)
75
+ return filename
@@ -0,0 +1,48 @@
1
+ """Command-line entry point: ``openpronounce <audio> "<expected text>"``."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+
8
+ def main(argv=None):
9
+ parser = argparse.ArgumentParser(
10
+ prog="openpronounce",
11
+ description="Score the pronunciation of an English recording against the sentence it should contain.",
12
+ )
13
+ parser.add_argument("audio", help="path to a wav/mp3/flac/ogg/webm recording")
14
+ parser.add_argument("text", help="the sentence the speaker was supposed to say")
15
+ parser.add_argument("--json", action="store_true", help="print the full JSON result instead of a summary")
16
+ parser.add_argument("--no-prosody", action="store_true", help="omit prosody contours from the JSON output")
17
+ args = parser.parse_args(argv)
18
+
19
+ from . import audio, speech
20
+
21
+ sound = audio.load(args.audio)
22
+ result = speech.compare_audio_with_text(sound, args.text)
23
+
24
+ if args.no_prosody:
25
+ result.pop("prosody", None)
26
+
27
+ if args.json:
28
+ json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
29
+ sys.stdout.write("\n")
30
+ return 0
31
+
32
+ print(f"Score : {result['score']}/100")
33
+ print(f"Transcription: {result['transcribe']}")
34
+ if "heard_phones" in result["differences"]:
35
+ print(f"Heard phones : /{' '.join(result['differences']['heard_phones'])}/")
36
+ errors = result["differences"]["errors"]
37
+ if errors:
38
+ print("Mispronounced:")
39
+ for err in errors:
40
+ actual = f"/{err['actual']}/" if err["actual"] else "(missing)"
41
+ print(f" - {err['word']}: expected /{err['expected']}/, heard {actual}")
42
+ else:
43
+ print("No mispronounced word detected.")
44
+ return 0
45
+
46
+
47
+ if __name__ == "__main__":
48
+ sys.exit(main())