JointRecAlign 0.1.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.
- jointrecalign-0.1.0/JointRecAlign.egg-info/PKG-INFO +535 -0
- jointrecalign-0.1.0/JointRecAlign.egg-info/SOURCES.txt +35 -0
- jointrecalign-0.1.0/JointRecAlign.egg-info/dependency_links.txt +1 -0
- jointrecalign-0.1.0/JointRecAlign.egg-info/entry_points.txt +3 -0
- jointrecalign-0.1.0/JointRecAlign.egg-info/requires.txt +19 -0
- jointrecalign-0.1.0/JointRecAlign.egg-info/top_level.txt +1 -0
- jointrecalign-0.1.0/LICENSE +21 -0
- jointrecalign-0.1.0/PKG-INFO +535 -0
- jointrecalign-0.1.0/README.md +494 -0
- jointrecalign-0.1.0/jointrecalign/__init__.py +1 -0
- jointrecalign-0.1.0/jointrecalign/__main__.py +4 -0
- jointrecalign-0.1.0/jointrecalign/download.py +82 -0
- jointrecalign-0.1.0/jointrecalign/evaluation/__init__.py +1 -0
- jointrecalign-0.1.0/jointrecalign/evaluation/attention_eval.py +214 -0
- jointrecalign-0.1.0/jointrecalign/evaluation/ctcfa_eval.py +131 -0
- jointrecalign-0.1.0/jointrecalign/evaluation/metrics.py +266 -0
- jointrecalign-0.1.0/jointrecalign/evaluation/sil_eval.py +311 -0
- jointrecalign-0.1.0/jointrecalign/evaluation/vad_chunking.py +166 -0
- jointrecalign-0.1.0/jointrecalign/model.py +609 -0
- jointrecalign-0.1.0/jointrecalign/modules.py +340 -0
- jointrecalign-0.1.0/jointrecalign/pool_alignment.py +958 -0
- jointrecalign-0.1.0/jointrecalign/praat.py +43 -0
- jointrecalign-0.1.0/jointrecalign/praat_plugin/setup.praat +2 -0
- jointrecalign-0.1.0/jointrecalign/praat_plugin/transcribe.praat +44 -0
- jointrecalign-0.1.0/jointrecalign/reference.py +204 -0
- jointrecalign-0.1.0/jointrecalign/report.py +193 -0
- jointrecalign-0.1.0/jointrecalign/report_assets/index.html +102 -0
- jointrecalign-0.1.0/jointrecalign/report_assets/page.html +507 -0
- jointrecalign-0.1.0/jointrecalign/sil_fs.py +278 -0
- jointrecalign-0.1.0/jointrecalign/transcribe.py +326 -0
- jointrecalign-0.1.0/jointrecalign/transcribe_cli.py +434 -0
- jointrecalign-0.1.0/pyproject.toml +60 -0
- jointrecalign-0.1.0/setup.cfg +4 -0
- jointrecalign-0.1.0/tests/test_model.py +189 -0
- jointrecalign-0.1.0/tests/test_reference.py +180 -0
- jointrecalign-0.1.0/tests/test_sil_fs.py +153 -0
- jointrecalign-0.1.0/tests/test_transcribe.py +355 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: JointRecAlign
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Joint phone recognition and alignment with wav2vec2 (CTC + cross-attention aligner)
|
|
5
|
+
Author: ImenBA
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Imenbaa/Joint_phoneme_recognition_alignment
|
|
8
|
+
Project-URL: Repository, https://github.com/Imenbaa/Joint_phoneme_recognition_alignment
|
|
9
|
+
Project-URL: Documentation, https://github.com/Imenbaa/Joint_phoneme_recognition_alignment/blob/main/GUIDE.md
|
|
10
|
+
Project-URL: Issues, https://github.com/Imenbaa/Joint_phoneme_recognition_alignment/issues
|
|
11
|
+
Project-URL: Model release, https://github.com/Imenbaa/Joint_phoneme_recognition_alignment/releases/tag/v0.1.0
|
|
12
|
+
Keywords: phone recognition,forced alignment,wav2vec2,CTC,speech,phonetics,French,Praat,TextGrid
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Natural Language :: French
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: torch>=2.1
|
|
24
|
+
Requires-Dist: torchaudio>=2.1
|
|
25
|
+
Requires-Dist: transformers>=4.41
|
|
26
|
+
Requires-Dist: numpy
|
|
27
|
+
Requires-Dist: soundfile
|
|
28
|
+
Requires-Dist: soxr
|
|
29
|
+
Requires-Dist: Levenshtein
|
|
30
|
+
Requires-Dist: jiwer
|
|
31
|
+
Requires-Dist: silero-vad>=6.2
|
|
32
|
+
Provides-Extra: train
|
|
33
|
+
Requires-Dist: datasets; extra == "train"
|
|
34
|
+
Requires-Dist: accelerate; extra == "train"
|
|
35
|
+
Requires-Dist: editdistance; extra == "train"
|
|
36
|
+
Requires-Dist: tensorboard; extra == "train"
|
|
37
|
+
Provides-Extra: eval
|
|
38
|
+
Requires-Dist: librosa; extra == "eval"
|
|
39
|
+
Requires-Dist: pandas; extra == "eval"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# A joint phoneme recognition and alignment model
|
|
43
|
+
|
|
44
|
+
Joint **phone recognition and phone alignment** for French speech, with one wav2vec2 model.
|
|
45
|
+
A CTC head transcribes the speech into phones. A cross-attention aligner, trained in the same
|
|
46
|
+
network, places each phone in time, with optional silences. The same model gives the phone
|
|
47
|
+
sequence and its segmentation, at 10 ms resolution.
|
|
48
|
+
|
|
49
|
+
This repository has the code to train the models from scratch, to evaluate them as aligners, and
|
|
50
|
+
to transcribe recordings into Praat TextGrids (phones, boundaries, pauses and the model's top-5
|
|
51
|
+
candidates for each phone), with no transcript needed.
|
|
52
|
+
|
|
53
|
+
**Phoneticians: start with the [step-by-step guide](https://github.com/Imenbaa/Joint_phoneme_recognition_alignment/blob/main/GUIDE.md)**: install with two commands, then
|
|
54
|
+
transcribe a corpus with one command (with an optional HTML report of the plots), or from Praat.
|
|
55
|
+
|
|
56
|
+
## Contents
|
|
57
|
+
|
|
58
|
+
- [Models](#models)
|
|
59
|
+
- [Installation](#installation)
|
|
60
|
+
- [Transcribing recordings](#transcribing-recordings)
|
|
61
|
+
- [Data](#data)
|
|
62
|
+
- [Training](#training)
|
|
63
|
+
- [Evaluation](#evaluation)
|
|
64
|
+
- [Tests](#tests)
|
|
65
|
+
- [Repository layout](#repository-layout)
|
|
66
|
+
|
|
67
|
+
## Models
|
|
68
|
+
|
|
69
|
+
Training runs in three stages. Each stage starts from the previous one.
|
|
70
|
+
|
|
71
|
+
| Stage | Model | Starts from | Frame | Recipe |
|
|
72
|
+
|---|---|---|---|---|
|
|
73
|
+
| 1 | **CTC 20 ms**: wav2vec2 phone recogniser | [LeBenchmark/wav2vec2-FR-7K-large](https://huggingface.co/LeBenchmark/wav2vec2-FR-7K-large) | 20 ms | `recipes/1_ctc_20ms.sh` |
|
|
74
|
+
| 2 | **CTC 10 ms**: same, last conv layer with stride 1 | CTC 20 ms | 10 ms | `recipes/2_ctc_10ms.sh` |
|
|
75
|
+
| 3 | **Joint aligner 10 ms + latent silence** | CTC 10 ms | 10 ms | `recipes/3_aligner_10ms_sil.sh` |
|
|
76
|
+
|
|
77
|
+
Stage 3 has two variants. They differ in how strongly the alignment losses train the encoder:
|
|
78
|
+
`ALPHA=0.1` (default) and `ALPHA=1.0`.
|
|
79
|
+
|
|
80
|
+
### CTC phone recogniser (stages 1 and 2)
|
|
81
|
+
|
|
82
|
+
`Wav2Vec2ForCTC` fine-tuned with CTC on phone sequences. The conv front-end is frozen, and the
|
|
83
|
+
epoch with the best validation phone error rate (PER) is kept.
|
|
84
|
+
|
|
85
|
+
Stage 2 changes the stride of the last conv layer from 2 to 1. Frames are then 10 ms instead
|
|
86
|
+
of 20 ms. Every pretrained weight still loads, and the model is fine-tuned again at the new
|
|
87
|
+
rate.
|
|
88
|
+
|
|
89
|
+
### Joint recognition + alignment model (stage 3)
|
|
90
|
+
|
|
91
|
+
`jointrecalign.model.Wav2Vec2ForCTCPoolContrastive`, built on the CTC 10 ms model:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
waveform ─► wav2vec2 encoder ─┬─► CTC head ──────────────────────────► phones (L_CTC)
|
|
95
|
+
│
|
|
96
|
+
├─► ConvBank (kernels 1,5,9) ─► queries ┐
|
|
97
|
+
│ ├─► attention energy (L_FS)
|
|
98
|
+
└─► phone keys ─────────────────────────┘ │
|
|
99
|
+
(CTC-posterior × Gaussian window ▼
|
|
100
|
+
pooling of encoder frames) masked reconstruction (L_m)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- **Phone keys.** Each phone of the target sequence gets one vector. It is a weighted average of
|
|
104
|
+
encoder frames: the weights are that phone's CTC posterior times a Gaussian window (σ = 10
|
|
105
|
+
frames) centred on the phone. No separate text encoder is needed.
|
|
106
|
+
- **L_FS: ForwardSum alignment loss.** It is computed on the frame × phone attention energy, with
|
|
107
|
+
a beta-binomial prior that is annealed away between 15 % and 50 % of training.
|
|
108
|
+
- **L_m: masked reconstruction loss.**
|
|
109
|
+
- The encoder frames are masked (p ~ U[0.1, 0.5]).
|
|
110
|
+
- They are reconstructed from the masked frames and the alignment-weighted phone keys.
|
|
111
|
+
- The reconstruction is scored with InfoNCE against codes from an EMA vector quantizer of the
|
|
112
|
+
encoder output (nearest-code assignment, 320 codes).
|
|
113
|
+
- A good reconstruction needs the right phone at the right frame, so L_m also trains the
|
|
114
|
+
alignment.
|
|
115
|
+
- **L_energy.** A small head predicts each frame's waveform log-energy from the alignment content
|
|
116
|
+
alone. It is added to L_m. A loud frame assigned to silence is then predicted quiet, and the
|
|
117
|
+
error corrects the alignment.
|
|
118
|
+
- **Latent optional silence (SIL)** starts at step 2000, in the style of Kaldi/MFA.
|
|
119
|
+
- SIL slots are inserted at word boundaries: `[SIL] p1 p2 [SIL] p3 … [SIL]`. They are optional
|
|
120
|
+
and all share one learned key.
|
|
121
|
+
- Entering a slot costs log p = −2, and a used SIL lasts at least 4 frames (40 ms).
|
|
122
|
+
- The ForwardSum dynamic programming sums over paths with and without each SIL
|
|
123
|
+
(`jointrecalign/sil_fs.py`).
|
|
124
|
+
- L_m takes its attention from the forward-backward occupancy.
|
|
125
|
+
- At inference, a Viterbi pass over the same topology gives phone boundaries and pauses.
|
|
126
|
+
- **Gradient routing.**
|
|
127
|
+
- L_CTC trains the whole encoder.
|
|
128
|
+
- L_FS and L_m reach only the top 2 transformer layers, through the queries.
|
|
129
|
+
- Their gradient is scaled by `ALPHA` (`--encoder_alpha`, `--lm_encoder_alpha`), ramped in
|
|
130
|
+
between steps 2000 and 5000.
|
|
131
|
+
- Phone keys and reconstruction targets are always detached.
|
|
132
|
+
|
|
133
|
+
At inference the model aligns its own CTC decode, so it needs no transcription.
|
|
134
|
+
|
|
135
|
+
## Installation
|
|
136
|
+
|
|
137
|
+
Python ≥ 3.10. A CUDA GPU for training; transcription also runs on CPU.
|
|
138
|
+
|
|
139
|
+
**To transcribe recordings only**, see the [step-by-step guide](https://github.com/Imenbaa/Joint_phoneme_recognition_alignment/blob/main/GUIDE.md) (no Python
|
|
140
|
+
knowledge needed), or, in an existing Python environment:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pip install JointRecAlign
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
(`pip install git+https://github.com/Imenbaa/Joint_phoneme_recognition_alignment.git` installs the
|
|
147
|
+
latest code from GitHub instead of the latest release.)
|
|
148
|
+
|
|
149
|
+
The model runs on PyTorch, and the PyTorch build that pip installs depends on the computer:
|
|
150
|
+
|
|
151
|
+
| Computer | pip installs | To do |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| Linux with an NVIDIA GPU | the CUDA build | nothing |
|
|
154
|
+
| Linux without an NVIDIA GPU | the CUDA build (about 5 GB of NVIDIA libraries it does not use) | install the CPU build first: `pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu` |
|
|
155
|
+
| Windows with an NVIDIA GPU | the CPU build (the GPU stays unused) | install a CUDA build first, with the command for your setup from [pytorch.org](https://pytorch.org/get-started/locally/) |
|
|
156
|
+
| macOS, Windows without an NVIDIA GPU | the right build | nothing |
|
|
157
|
+
|
|
158
|
+
When torch is already installed, `pip install JointRecAlign` keeps it. With uv,
|
|
159
|
+
`uv pip install --torch-backend auto JointRecAlign` chooses the build that matches the computer
|
|
160
|
+
(GUIDE.md does the same with `uv tool install`).
|
|
161
|
+
The evaluation scripts also need `pip install "JointRecAlign[eval]"` (librosa, pandas), which
|
|
162
|
+
`requirements.txt` covers.
|
|
163
|
+
|
|
164
|
+
Both install the `JointRecAlign` command.
|
|
165
|
+
|
|
166
|
+
**To train or evaluate**, clone the repository:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
git clone https://github.com/Imenbaa/Joint_phoneme_recognition_alignment.git
|
|
170
|
+
cd Joint_phoneme_recognition_alignment
|
|
171
|
+
python -m venv .venv && source .venv/bin/activate
|
|
172
|
+
pip install -r requirements.txt # versions used to train the models
|
|
173
|
+
pip install -e . # the jointrecalign package
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The scripts at the repository root (`train_ctc.py`, `train_aligner.py`, `evaluate.py`,
|
|
177
|
+
`transcribe.py`) also run from the repository root without `pip install -e .`.
|
|
178
|
+
`pip install -e .` also installs the `JointRecAlign` command.
|
|
179
|
+
|
|
180
|
+
## Transcribing recordings
|
|
181
|
+
|
|
182
|
+
`JointRecAlign` (or `python transcribe.py` from the repository root) gives, for each
|
|
183
|
+
recording, the phones the model hears, their boundaries, the pauses, and the model's five best
|
|
184
|
+
candidates for each phone. It runs on GPU or CPU.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
JointRecAlign validate recordings_folder/ # check the audio files (no model)
|
|
188
|
+
JointRecAlign transcribe recordings_folder/ [-o out/] # check, then transcribe
|
|
189
|
+
JointRecAlign transcribe recordings_folder/ --report # + one HTML page per recording, index.html
|
|
190
|
+
JointRecAlign transcribe recordings_folder/ --reference textgrids/ # + comparison with reference TextGrids
|
|
191
|
+
JointRecAlign install-praat-plugin # a "JointRecAlign: transcribe..." button for Sounds in Praat
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`transcribe` checks every file first (readable, at least 0.5 s, not silent; warnings for a
|
|
195
|
+
sampling rate under 16 kHz, several channels, clipping, very low level, two recordings that
|
|
196
|
+
would write the same result files), skips the files that cannot be transcribed, goes on when
|
|
197
|
+
one fails, and ends with a summary, also written to `JointRecAlign.log`. The exit code is 1 when
|
|
198
|
+
a recording was not transcribed.
|
|
199
|
+
|
|
200
|
+
**The model** ([`aligner_10ms_sil_alpha0.1.zip`](https://github.com/Imenbaa/Joint_phoneme_recognition_alignment/releases/download/v0.1.0/aligner_10ms_sil_alpha0.1.zip),
|
|
201
|
+
1.2 GB, [release v0.1.0](https://github.com/Imenbaa/Joint_phoneme_recognition_alignment/releases/tag/v0.1.0),
|
|
202
|
+
licence CC BY-NC 4.0) is downloaded and checked the first time (or with `JointRecAlign download`)
|
|
203
|
+
into a cache folder: `~/.cache/JointRecAlign` (Linux), `~/Library/Caches/JointRecAlign` (macOS),
|
|
204
|
+
`%LOCALAPPDATA%\JointRecAlign` (Windows); `JOINTRECALIGN_HOME` changes it. Any stage-3 checkpoint
|
|
205
|
+
you train yourself (joint aligner with latent silence, see [Training](#training)) works with
|
|
206
|
+
`--model`, e.g. `--model $WORK_DIR/aligner_10ms_sil_alpha0.1/checkpoint-43000`.
|
|
207
|
+
|
|
208
|
+
Audio: WAV, FLAC, MP3, OGG or AIFF, any sampling rate, mono or stereo (resampled to 16 kHz and
|
|
209
|
+
downmixed). A folder means every audio file in it. A recording that cannot be read is reported
|
|
210
|
+
and the others are still transcribed.
|
|
211
|
+
|
|
212
|
+
For each recording, in a folder `JointRecAlign/` next to the recording (or in `-o`), and
|
|
213
|
+
`JointRecAlign.log`:
|
|
214
|
+
|
|
215
|
+
| file | content |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `<name>.TextGrid` | Praat interval tiers, all covering the whole recording |
|
|
218
|
+
| `<name>.csv` | one row per phone: `start`, `end`, `phone`, then `top1`, `p1` ... `top5`, `p5` |
|
|
219
|
+
| `<name>.html` | with `--report`: waveform, phones and pauses, the aligner's attention with the alignment path, the CTC posteriors of every phone, the five candidates of each phone coloured by probability, on one time axis that scrolls and zooms; self-contained, no server (`index.html` lists the recordings) |
|
|
220
|
+
|
|
221
|
+
| tier | label of each interval |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `phones` | the recognised phone (empty between phones) |
|
|
224
|
+
| `confidence` | probability of that phone |
|
|
225
|
+
| `top2` ... `top5` | the next candidates, as `symbol probability` (e.g. `p 0.050`) |
|
|
226
|
+
| `pauses` | `SIL` on every pause the model detected |
|
|
227
|
+
|
|
228
|
+
| option | meaning |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `-o folder` | where the results go (default: `JointRecAlign/` next to each recording) |
|
|
231
|
+
| `--report` | also the HTML pages (about 0.6 MB per minute of audio) |
|
|
232
|
+
| `--copy_audio` | with `--report`: copy each recording into `<results>/audio/`, so the pages still play it after the results folder is moved or copied to another computer |
|
|
233
|
+
| `--model folder` | another model (default: the released one, downloaded the first time) |
|
|
234
|
+
| `--symbols ipa` (default) / `model` | `ipa` writes the nasal vowels and œ in IPA (`ɑ̃ ɛ̃ ɔ̃ œ`); `model` keeps the model's one-character codes (`@ 1 § 9`) |
|
|
235
|
+
| `--phone_map map.json` | your own labels, applied last, e.g. `{"ə": "ø"}` for a corpus that writes the schwa `ø` |
|
|
236
|
+
| `--max_chunk 10` | longest piece of audio the model sees at once, in seconds |
|
|
237
|
+
| `--vocab_path` | phone vocabulary (default: `vocab.json` in the checkpoint) |
|
|
238
|
+
| `--device cuda` / `cpu` | default: `cuda` when available |
|
|
239
|
+
|
|
240
|
+
**Comparison with a reference** (`--reference FOLDER`, `jointrecalign/reference.py`): for
|
|
241
|
+
`<name>.wav`, the phone tier of `FOLDER/<name>.TextGrid` (`--ref_tier`, default the tier named
|
|
242
|
+
`phones`/`phonemes`/..., else the first interval tier) is compared with the transcription with the
|
|
243
|
+
metrics of the evaluation (`evaluate.py`):
|
|
244
|
+
|
|
245
|
+
| figure | meaning |
|
|
246
|
+
|---|---|
|
|
247
|
+
| PER | substitutions + deletions + insertions over the reference phones |
|
|
248
|
+
| F1 20 ms / 50 ms | boundary detection: every phone onset and offset; each reference boundary is matched one-to-one to the nearest recognised boundary within 20 / 50 ms |
|
|
249
|
+
| median error | median \|error\| of the starts and ends of the phones recognised with the reference label |
|
|
250
|
+
| pause P / R / F1 | reference pauses matched one-to-one by a recognised pause (time not covered by a phone) with intersection over union ≥ 0.5 |
|
|
251
|
+
|
|
252
|
+
Empty intervals and `--pause_labels` (default `SIL sil sp spn _ # <p:>` ...) are reference pauses,
|
|
253
|
+
`--skip_labels` (e.g. `noise,overlap`) are left out, and `--ref_map map.json` rewrites the other
|
|
254
|
+
labels into the model's phones (e.g. `{"ø": "ə"}` for a corpus that writes the schwa `ø`). The
|
|
255
|
+
summary lists the reference labels that are not phones of the model; they count as errors. Results:
|
|
256
|
+
`comparison.csv` (one row per recording and a `TOTAL` row; corpus figures pool the counts of all
|
|
257
|
+
recordings), the scores in the summary and, with `--report`, in the pages: reference lane under the
|
|
258
|
+
recognised phones (amber: substituted, red: inserted / deleted), reference boundaries and pauses
|
|
259
|
+
in red on the attention, reference phones as red boxes on the posteriors, on the top 5 a reference
|
|
260
|
+
strip with each recognised phone's candidates outlined in green (reference = 1st) or red
|
|
261
|
+
(reference among the others), and a "show reference" switch. A reference TextGrid is never overwritten (`-o` into the reference folder
|
|
262
|
+
is refused when the names collide).
|
|
263
|
+
|
|
264
|
+
How it works (`jointrecalign/transcribe.py`):
|
|
265
|
+
1. The recording is cut into pieces of at most 10 s, back to back, so no audio is dropped.
|
|
266
|
+
[Silero VAD](https://github.com/snakers4/silero-vad) finds the pauses; each cut goes in the
|
|
267
|
+
middle of the latest pause within 10 s (or, with no pause, where speech is least likely).
|
|
268
|
+
2. For each piece: the CTC head gives the phones; a Viterbi pass over
|
|
269
|
+
`[SIL] p1 [SIL] p2 ... [SIL]` on the aligner gives their boundaries and the pauses, as in the
|
|
270
|
+
evaluation. A pause cut in two by a piece boundary is joined back into one.
|
|
271
|
+
3. The candidates of a phone are the CTC posteriors at the frame where that phone is most
|
|
272
|
+
probable, over the phone symbols only, renormalised to sum to 1.
|
|
273
|
+
|
|
274
|
+
Read the probabilities with care: they are the model's own and are **not calibrated**. The top-1
|
|
275
|
+
is often given about 0.99 even when it is wrong, but the **order** of the candidates is
|
|
276
|
+
informative: when the top-1 is wrong, the right phone is 2nd in 29 to 57 % of the cases and in the
|
|
277
|
+
top 5 in 53 to 79 %, depending on the kind of speech (measured against reference transcriptions; see
|
|
278
|
+
GUIDE.md, "How far to trust the candidates").
|
|
279
|
+
|
|
280
|
+
The model cannot see the first and last ~10 ms of each piece, so a gap of 15 to 25 ms with no
|
|
281
|
+
label is left at each cut (about every 10 s) and at the start and end of the recording.
|
|
282
|
+
|
|
283
|
+
## Data
|
|
284
|
+
|
|
285
|
+
### Training corpus
|
|
286
|
+
|
|
287
|
+
The models were trained on French speech with phone transcriptions. Utterances containing an
|
|
288
|
+
unknown word were removed. Any French (or other) corpus with phone-level transcriptions can be used
|
|
289
|
+
in the same format.
|
|
290
|
+
|
|
291
|
+
### Format
|
|
292
|
+
|
|
293
|
+
The code reads a Hugging Face [`audiofolder`](https://huggingface.co/docs/datasets/audio_dataset#audiofolder):
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
DATA_DIR/
|
|
297
|
+
train/ metadata.csv + audio files
|
|
298
|
+
dev/ (or validation/)
|
|
299
|
+
test/
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`metadata.csv` has one row per utterance:
|
|
303
|
+
|
|
304
|
+
| column | required | content |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| `file_name` | yes | audio file, relative to the split folder |
|
|
307
|
+
| `phonemes_single_v2` | yes | the phone sequence, one symbol per phone, separated by spaces: `p a t ʁ i s j a` |
|
|
308
|
+
| `phonemes_single` | stage 3 | the same phones written as one string per word, words separated by spaces: `patʁisja maʁt1`. It places the SIL slots at word boundaries, so each phone symbol must be a single character, identical in both columns |
|
|
309
|
+
| `phonemes` | no | if present, rows containing `\|ø\|n\|k\|` (unknown word) are dropped |
|
|
310
|
+
|
|
311
|
+
Any other column (for example `transcription`) is ignored. Producing the phone transcriptions
|
|
312
|
+
(lexicon / grapheme-to-phoneme conversion) is not part of this repository. The phone vocabulary
|
|
313
|
+
is built from the data in stage 0.
|
|
314
|
+
|
|
315
|
+
## Training
|
|
316
|
+
|
|
317
|
+
Every recipe reads two environment variables. Settings can be overridden with extra flags,
|
|
318
|
+
which are passed through to the Python script.
|
|
319
|
+
|
|
320
|
+
- `WORK_DIR`: receives the preprocessed data and all models.
|
|
321
|
+
- `DATA_DIR`: the audiofolder above.
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
export DATA_DIR=/path/to/audiofolder WORK_DIR=/path/to/work
|
|
325
|
+
|
|
326
|
+
bash recipes/0_prepare_data.sh # -> $WORK_DIR/data/{preprocessed_16k, vocab.json}
|
|
327
|
+
NGPU=2 bash recipes/1_ctc_20ms.sh # -> $WORK_DIR/ctc_20ms
|
|
328
|
+
bash recipes/2_ctc_10ms.sh # -> $WORK_DIR/ctc_10ms
|
|
329
|
+
bash recipes/3_aligner_10ms_sil.sh # -> $WORK_DIR/aligner_10ms_sil_alpha0.1
|
|
330
|
+
ALPHA=1.0 bash recipes/3_aligner_10ms_sil.sh # -> $WORK_DIR/aligner_10ms_sil_alpha1.0
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Choose the GPU with `CUDA_VISIBLE_DEVICES`. Stage 1 uses `torchrun` with `NGPU` processes. The
|
|
334
|
+
gradient accumulation adapts so that the effective batch stays 128. TensorBoard logs go to
|
|
335
|
+
`<output>/runs`.
|
|
336
|
+
|
|
337
|
+
### Stage 0: data preparation (`train_ctc.py --preprocess_only`)
|
|
338
|
+
|
|
339
|
+
The audio is resampled to 16 kHz and normalised, and the phone strings are converted to ids.
|
|
340
|
+
The result is an Arrow dataset, plus a vocabulary file `vocab.json` (`[PAD]` = CTC blank,
|
|
341
|
+
`[UNK]`, then the phones). All later stages read this dataset.
|
|
342
|
+
|
|
343
|
+
### Stages 1–2: CTC (`train_ctc.py`)
|
|
344
|
+
|
|
345
|
+
| | stage 1 | stage 2 |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| `--init_model` | `LeBenchmark/wav2vec2-FR-7K-large` | `$WORK_DIR/ctc_20ms` |
|
|
348
|
+
| `--last_conv_stride` | 2 (20 ms) | 1 (10 ms) |
|
|
349
|
+
| `--epochs` | 30 | 10 |
|
|
350
|
+
| `--lr` | 3e-5 (linear decay, 500 warm-up steps) | 3e-5 |
|
|
351
|
+
| batch | 32 × 2 GPUs × grad_acc 2 = 128 | 16 × grad_acc 4 = 64 |
|
|
352
|
+
| precision | bf16 on Ampere GPUs, else fp16 | same |
|
|
353
|
+
|
|
354
|
+
Other settings, the same for both stages:
|
|
355
|
+
- frozen conv front-end (`--train_cnn` unfreezes it);
|
|
356
|
+
- weight decay 0.005, gradient clipping 2.0;
|
|
357
|
+
- gradient checkpointing;
|
|
358
|
+
- the epoch with the best validation PER is kept.
|
|
359
|
+
|
|
360
|
+
Run `python train_ctc.py --help` for every option.
|
|
361
|
+
|
|
362
|
+
### Stage 3: joint aligner (`train_aligner.py`)
|
|
363
|
+
|
|
364
|
+
Main settings of `recipes/3_aligner_10ms_sil.sh`:
|
|
365
|
+
|
|
366
|
+
| group | flags |
|
|
367
|
+
|---|---|
|
|
368
|
+
| length, batch | `--max_steps 43750`, batch 4 × grad_acc 8 = 32, `--fp16`, `--seed 42` |
|
|
369
|
+
| learning rates | encoder + CTC head `--backbone_lr 1e-5`, aligner `--alignment_lr 1e-4`, weight decay 1e-4, no warm-up, clipping 1.0 |
|
|
370
|
+
| losses | `--ctc_weight 1 --align_weight 1 --contrastive_weight 1`; L_FS ramps in over `--align_warmup_steps 1000` |
|
|
371
|
+
| aligner | `--pool_mode window --window_sigma_frames 10 --convbank_kernels 1,5,9`; attention dimension = encoder size / 2 |
|
|
372
|
+
| prior | `--prior_type beta_binomial --prior_scaling_factor 0.5 --prior_schedule anneal` from 0.15 to 0.5 of training |
|
|
373
|
+
| L_m | `--quantizer_assign nearest --lm_mask_same_code --no_position_features`, 50 negatives, temperature 0.1 |
|
|
374
|
+
| L_energy | `--lm_energy_weight 1.0` |
|
|
375
|
+
| routing | `--encoder_alpha ALPHA --lm_encoder_alpha ALPHA --encoder_top_layers 2`, ramp `--encoder_alpha_start_steps 2000 --encoder_alpha_ramp_steps 3000` |
|
|
376
|
+
| silence | `--sil --sil_positions word --sil_logp -2.0 --sil_min_frames 4 --sil_init lowenergy --sil_start_steps 2000 --lm_att_source fs_posterior` |
|
|
377
|
+
| bookkeeping | checkpoint every 1000 steps (`--optimizer_last_only` keeps `optimizer.pt` in the newest one only); validation PER every 1000 steps |
|
|
378
|
+
|
|
379
|
+
Run `python train_aligner.py --help` for every option and its meaning.
|
|
380
|
+
|
|
381
|
+
Notes:
|
|
382
|
+
- **Held-out alignment metrics.** `EVAL_MANIFEST=/path/to/manifest.json` (see
|
|
383
|
+
[Evaluation](#evaluation)) also scores each checkpoint as an aligner on a held-out manifest.
|
|
384
|
+
The scores are logged to TensorBoard under `heldout/` and saved in `<output>/heldout/`. They
|
|
385
|
+
are not used to select checkpoints.
|
|
386
|
+
- **Resuming.** Training resumes from the newest checkpoint in the output folder, so a run that
|
|
387
|
+
is stopped can be restarted with the same command.
|
|
388
|
+
- **Expected warning.** The trainer warns that 7 trainable tensors are in no optimizer group.
|
|
389
|
+
These are the wav2vec2 pretraining heads (`quantizer`, `project_hid`, `project_q`), which this
|
|
390
|
+
model does not use.
|
|
391
|
+
|
|
392
|
+
## Evaluation
|
|
393
|
+
|
|
394
|
+
`evaluate.py` scores checkpoints as phone aligners on a **manifest** of long recordings with
|
|
395
|
+
reference phone intervals:
|
|
396
|
+
|
|
397
|
+
```json
|
|
398
|
+
{"files": {
|
|
399
|
+
"rec01": {
|
|
400
|
+
"wav": "/path/to/rec01.wav",
|
|
401
|
+
"chunks": [{"start": 0.0, "end": 9.8}, {"start": 15.1, "end": 29.8}],
|
|
402
|
+
"ref_intervals": [{"phoneme": "b", "start": 0.31, "end": 0.38}, ...],
|
|
403
|
+
"style": "spontaneous",
|
|
404
|
+
"pause_intervals": [[3.10, 3.42], ...],
|
|
405
|
+
"excluded_intervals": [[20.0, 21.5], ...]
|
|
406
|
+
}
|
|
407
|
+
}}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
| field | required | meaning |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| `wav` | yes | the recording; any sampling rate, mono or stereo |
|
|
413
|
+
| `chunks` | yes | the segments the model sees, in seconds (see [Building a manifest](#building-a-manifest)) |
|
|
414
|
+
| `ref_intervals` | yes | the reference phones and their intervals |
|
|
415
|
+
| `style` | no | groups the results by speaking style |
|
|
416
|
+
| `pause_intervals` | no | reference pauses; enables the silence scores |
|
|
417
|
+
| `excluded_intervals` | no | regions ignored by the silence scores |
|
|
418
|
+
|
|
419
|
+
### Building a manifest
|
|
420
|
+
|
|
421
|
+
`make_manifest.py` builds a manifest from a folder of recordings and Praat TextGrids that have a
|
|
422
|
+
phone tier. The chunks come from the [WhisperX](https://github.com/m-bain/whisperX) voice
|
|
423
|
+
activity detector (pyannote segmentation, onset 0.5 / offset 0.363). Speech is cut and merged
|
|
424
|
+
into chunks of at most 10 s, with pauses kept inside a chunk. WhisperX pins its own `torch`
|
|
425
|
+
version, so install it in a separate environment:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
python -m venv .venv-manifest && source .venv-manifest/bin/activate
|
|
429
|
+
pip install -r requirements-manifest.txt
|
|
430
|
+
|
|
431
|
+
python make_manifest.py --audio_dir wav/ --textgrid_dir textgrids/ --tier phones \
|
|
432
|
+
--phone_map phone_map.json --skip_labels "<noise>" "<unk>" \
|
|
433
|
+
--style=_read=read,_spont=spontaneous --out manifest.json
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
| option | meaning |
|
|
437
|
+
|---|---|
|
|
438
|
+
| `--textgrid_suffix` | TextGrid name = audio id + suffix (default `.TextGrid`). Write `--textgrid_suffix=-x.TextGrid` when it starts with `-` |
|
|
439
|
+
| `--phone_map` | JSON `{"tier label": "phone"}` that rewrites the reference labels into the model's phone set (`vocab.json`). Unmapped labels are kept |
|
|
440
|
+
| `--pause_labels` | labels of pauses (default: empty, `SIL`, `sil`, `spn`, `SP`, `<SIL>`, `_`). They become `pause_intervals` |
|
|
441
|
+
| `--skip_labels` | other non-phone labels (noise, unknown words, ...). They become `excluded_intervals` |
|
|
442
|
+
| `--style` | `SUBSTRING=STYLE` rules on the file id, comma-separated, for per-style results |
|
|
443
|
+
| `--max_chunk_dur` | maximum chunk length in seconds (default 10) |
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
python evaluate.py --manifest manifest.json --out_dir eval/ \
|
|
447
|
+
$WORK_DIR/aligner_10ms_sil_alpha0.1/checkpoint-43000
|
|
448
|
+
python evaluate.py --manifest manifest.json --out_dir eval/ --method ctcfa_contig <checkpoint>
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
- **What is aligned.** The model aligns its own CTC greedy decode, not the reference phones.
|
|
452
|
+
Hypothesis and reference are matched by edit distance.
|
|
453
|
+
- **`--method attention`** (default):
|
|
454
|
+
- for a latent-SIL model, Viterbi decoding over phones and optional SIL;
|
|
455
|
+
- otherwise, monotonic DTW on the attention.
|
|
456
|
+
- For SIL models the plain DTW decode of the same pass is also reported, with the suffix
|
|
457
|
+
`_dtw`.
|
|
458
|
+
- **`--method ctcfa` / `ctcfa_contig`.** The same decoded phones are force-aligned on the CTC
|
|
459
|
+
head with `torchaudio.functional.forced_align`. Phones either end where their CTC run ends
|
|
460
|
+
(`ctcfa`) or where the next phone starts (`ctcfa_contig`). This is a baseline that does not
|
|
461
|
+
use the attention.
|
|
462
|
+
- **Frame timing** is read from the model config (stride and receptive field): frame f of a chunk
|
|
463
|
+
starting at t₀ gives a boundary at t₀ + f·stride + (RF − stride)/2. That is +7.5 ms at 10 ms
|
|
464
|
+
and +2.5 ms at 20 ms. The same rule is used for every method.
|
|
465
|
+
|
|
466
|
+
**Metrics** (`jointrecalign/evaluation/metrics.py`):
|
|
467
|
+
- boundary precision, recall and F1 within 10, 20 and 50 ms (main score: F1@20);
|
|
468
|
+
- median, mean and 90th percentile of the absolute boundary error, and rate of errors > 50 ms;
|
|
469
|
+
- signed shift of phone starts and ends;
|
|
470
|
+
- PER;
|
|
471
|
+
- duration fidelity: slope and correlation of hypothesis versus reference phone durations
|
|
472
|
+
(`_sp`: phones with no pause on either side);
|
|
473
|
+
- for SIL models with `pause_intervals`: frame-level SIL precision/recall/F1 and pause-level F1
|
|
474
|
+
at IoU ≥ 0.5, over each whole file: a pause cut by a chunk boundary counts once, and audio left
|
|
475
|
+
out between chunks counts as silence (`sil_eval.evaluate(..., sil_mode="chunk")` gives the
|
|
476
|
+
older per-chunk scores, which count such a pause as two false silences).
|
|
477
|
+
|
|
478
|
+
The script writes these files to `--out_dir`:
|
|
479
|
+
- per-file, per-style and global CSVs;
|
|
480
|
+
- the alignments (`.pkl`);
|
|
481
|
+
- one JSON line per checkpoint in `summary.jsonl`.
|
|
482
|
+
|
|
483
|
+
## Tests
|
|
484
|
+
|
|
485
|
+
CPU only. They need no data or pretrained weights:
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
python tests/test_sil_fs.py # latent-SIL DP vs brute force, gradients, batching, == ForwardSumLoss without SIL
|
|
489
|
+
python tests/test_model.py # full model on a tiny random wav2vec2: losses, gradient routing, SIL, L_energy, decoders
|
|
490
|
+
python tests/test_transcribe.py # chunking, pause joining, candidates, TextGrid/CSV, end to end on a tiny model
|
|
491
|
+
python tests/test_reference.py # TextGrid reading, reference labels, comparison scores
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
The end-to-end part of `test_transcribe.py` needs `silero-vad` (in `requirements.txt`) and is
|
|
495
|
+
skipped without it.
|
|
496
|
+
|
|
497
|
+
## Repository layout
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
train_ctc.py stages 0-2: data preparation, CTC fine-tuning (20 or 10 ms)
|
|
501
|
+
train_aligner.py stage 3: joint recognition + alignment model
|
|
502
|
+
evaluate.py alignment evaluation on a manifest
|
|
503
|
+
make_manifest.py evaluation manifest from audio + TextGrids (WhisperX VAD chunks)
|
|
504
|
+
transcribe.py recordings -> TextGrids (= JointRecAlign)
|
|
505
|
+
GUIDE.md step-by-step guide for phoneticians
|
|
506
|
+
recipes/ one script per stage, with the settings of the trained models
|
|
507
|
+
jointrecalign/
|
|
508
|
+
transcribe.py Silero chunking, decoding, top-5 candidates, TextGrid / CSV writers
|
|
509
|
+
transcribe_cli.py the JointRecAlign command
|
|
510
|
+
report.py, report_assets/ the HTML report
|
|
511
|
+
reference.py comparison with reference TextGrids
|
|
512
|
+
download.py download of the released model into a cache folder
|
|
513
|
+
praat.py, praat_plugin/ the Praat plugin
|
|
514
|
+
modules.py ForwardSum loss, ConvBank, cross-attention, prior helpers
|
|
515
|
+
pool_alignment.py CTC + aligner with pooled phone keys, latent SIL, gradient routing
|
|
516
|
+
model.py + masked reconstruction (L_m), EMA quantizer, L_energy
|
|
517
|
+
sil_fs.py latent optional SIL: topology, forward-backward, Viterbi
|
|
518
|
+
evaluation/
|
|
519
|
+
attention_eval.py manifest loop, DTW decode, frame timing
|
|
520
|
+
sil_eval.py SIL Viterbi decode and silence scores
|
|
521
|
+
ctcfa_eval.py CTC forced-alignment baseline
|
|
522
|
+
vad_chunking.py WhisperX VAD chunking (used by make_manifest.py)
|
|
523
|
+
metrics.py boundary F1, errors, PER
|
|
524
|
+
tests/
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
## Generative AI Use Disclosure
|
|
528
|
+
|
|
529
|
+
The code in this repository was refactored, organised and refined with the aid of
|
|
530
|
+
[Claude Code](https://claude.com/claude-code) (Anthropic).
|
|
531
|
+
|
|
532
|
+
## License
|
|
533
|
+
|
|
534
|
+
Code: MIT, see `LICENSE`. Released model weights: [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/)
|
|
535
|
+
(non-commercial use, with attribution).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
JointRecAlign.egg-info/PKG-INFO
|
|
5
|
+
JointRecAlign.egg-info/SOURCES.txt
|
|
6
|
+
JointRecAlign.egg-info/dependency_links.txt
|
|
7
|
+
JointRecAlign.egg-info/entry_points.txt
|
|
8
|
+
JointRecAlign.egg-info/requires.txt
|
|
9
|
+
JointRecAlign.egg-info/top_level.txt
|
|
10
|
+
jointrecalign/__init__.py
|
|
11
|
+
jointrecalign/__main__.py
|
|
12
|
+
jointrecalign/download.py
|
|
13
|
+
jointrecalign/model.py
|
|
14
|
+
jointrecalign/modules.py
|
|
15
|
+
jointrecalign/pool_alignment.py
|
|
16
|
+
jointrecalign/praat.py
|
|
17
|
+
jointrecalign/reference.py
|
|
18
|
+
jointrecalign/report.py
|
|
19
|
+
jointrecalign/sil_fs.py
|
|
20
|
+
jointrecalign/transcribe.py
|
|
21
|
+
jointrecalign/transcribe_cli.py
|
|
22
|
+
jointrecalign/evaluation/__init__.py
|
|
23
|
+
jointrecalign/evaluation/attention_eval.py
|
|
24
|
+
jointrecalign/evaluation/ctcfa_eval.py
|
|
25
|
+
jointrecalign/evaluation/metrics.py
|
|
26
|
+
jointrecalign/evaluation/sil_eval.py
|
|
27
|
+
jointrecalign/evaluation/vad_chunking.py
|
|
28
|
+
jointrecalign/praat_plugin/setup.praat
|
|
29
|
+
jointrecalign/praat_plugin/transcribe.praat
|
|
30
|
+
jointrecalign/report_assets/index.html
|
|
31
|
+
jointrecalign/report_assets/page.html
|
|
32
|
+
tests/test_model.py
|
|
33
|
+
tests/test_reference.py
|
|
34
|
+
tests/test_sil_fs.py
|
|
35
|
+
tests/test_transcribe.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jointrecalign
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ImenBA
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|