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.
Files changed (37) hide show
  1. jointrecalign-0.1.0/JointRecAlign.egg-info/PKG-INFO +535 -0
  2. jointrecalign-0.1.0/JointRecAlign.egg-info/SOURCES.txt +35 -0
  3. jointrecalign-0.1.0/JointRecAlign.egg-info/dependency_links.txt +1 -0
  4. jointrecalign-0.1.0/JointRecAlign.egg-info/entry_points.txt +3 -0
  5. jointrecalign-0.1.0/JointRecAlign.egg-info/requires.txt +19 -0
  6. jointrecalign-0.1.0/JointRecAlign.egg-info/top_level.txt +1 -0
  7. jointrecalign-0.1.0/LICENSE +21 -0
  8. jointrecalign-0.1.0/PKG-INFO +535 -0
  9. jointrecalign-0.1.0/README.md +494 -0
  10. jointrecalign-0.1.0/jointrecalign/__init__.py +1 -0
  11. jointrecalign-0.1.0/jointrecalign/__main__.py +4 -0
  12. jointrecalign-0.1.0/jointrecalign/download.py +82 -0
  13. jointrecalign-0.1.0/jointrecalign/evaluation/__init__.py +1 -0
  14. jointrecalign-0.1.0/jointrecalign/evaluation/attention_eval.py +214 -0
  15. jointrecalign-0.1.0/jointrecalign/evaluation/ctcfa_eval.py +131 -0
  16. jointrecalign-0.1.0/jointrecalign/evaluation/metrics.py +266 -0
  17. jointrecalign-0.1.0/jointrecalign/evaluation/sil_eval.py +311 -0
  18. jointrecalign-0.1.0/jointrecalign/evaluation/vad_chunking.py +166 -0
  19. jointrecalign-0.1.0/jointrecalign/model.py +609 -0
  20. jointrecalign-0.1.0/jointrecalign/modules.py +340 -0
  21. jointrecalign-0.1.0/jointrecalign/pool_alignment.py +958 -0
  22. jointrecalign-0.1.0/jointrecalign/praat.py +43 -0
  23. jointrecalign-0.1.0/jointrecalign/praat_plugin/setup.praat +2 -0
  24. jointrecalign-0.1.0/jointrecalign/praat_plugin/transcribe.praat +44 -0
  25. jointrecalign-0.1.0/jointrecalign/reference.py +204 -0
  26. jointrecalign-0.1.0/jointrecalign/report.py +193 -0
  27. jointrecalign-0.1.0/jointrecalign/report_assets/index.html +102 -0
  28. jointrecalign-0.1.0/jointrecalign/report_assets/page.html +507 -0
  29. jointrecalign-0.1.0/jointrecalign/sil_fs.py +278 -0
  30. jointrecalign-0.1.0/jointrecalign/transcribe.py +326 -0
  31. jointrecalign-0.1.0/jointrecalign/transcribe_cli.py +434 -0
  32. jointrecalign-0.1.0/pyproject.toml +60 -0
  33. jointrecalign-0.1.0/setup.cfg +4 -0
  34. jointrecalign-0.1.0/tests/test_model.py +189 -0
  35. jointrecalign-0.1.0/tests/test_reference.py +180 -0
  36. jointrecalign-0.1.0/tests/test_sil_fs.py +153 -0
  37. 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,3 @@
1
+ [console_scripts]
2
+ JointRecAlign = jointrecalign.transcribe_cli:main
3
+ JointRecAlign-transcribe = jointrecalign.transcribe_cli:main_transcribe
@@ -0,0 +1,19 @@
1
+ torch>=2.1
2
+ torchaudio>=2.1
3
+ transformers>=4.41
4
+ numpy
5
+ soundfile
6
+ soxr
7
+ Levenshtein
8
+ jiwer
9
+ silero-vad>=6.2
10
+
11
+ [eval]
12
+ librosa
13
+ pandas
14
+
15
+ [train]
16
+ datasets
17
+ accelerate
18
+ editdistance
19
+ tensorboard
@@ -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.