Skip to content

Instantly share code, notes, and snippets.

@lumaku
Last active June 25, 2023 20:07
Show Gist options
  • Save lumaku/75eca1c86d9467a54888d149dc7b84f1 to your computer and use it in GitHub Desktop.
Save lumaku/75eca1c86d9467a54888d149dc7b84f1 to your computer and use it in GitHub Desktop.
A short introduction how to use CTC segmentation with Speechbrain

CTC segmentation for Speechbrain

CTC segmentation can be used to align utterances in long audio files. This gist introduces how to use CTC segmentation for Speechbrain.

A short example

from speechbrain.pretrained import EncoderDecoderASR
from speechbrain.alignment.ctc_segmentation import CTCSegmentation

# Requires a model with CTC output
asr_model = EncoderDecoderASR.from_hparams(source="speechbrain/asr-transformer-transformerlm-librispeech")
aligner = CTCSegmentation(asr_model, kaldi_style_text=False)

Example 1 with the example file included in the speechbrain repository

audio_path = "./samples/audio_samples/example1.wav"
text = ["THE BIRCH CANOE", "SLID ON THE", "SMOOTH PLANKS"]
segments = aligner(audio_path, text, name="example1")
print(segments)
# example1_0000 example1 0.04 0.70 -0.0122 THE BIRCH CANOE
# example1_0001 example1 0.97 1.97 -0.0295 SLID ON THE
# example1_0002 example1 1.97 3.00 -0.0258 SMOOTH PLANKS

The output is a list of utterances with timings in seconds, score and text. The formatting:

# utterance_name file_name start stop score utterance_text

Example 2 from mini-librispeech:

# !wget https://www.openslr.org/resources/31/dev-clean-2.tar.gz
# !tar -xvzf dev-clean-2.tar.gz
audio_path = "./LibriSpeech/dev-clean-2/1272/135031/1272-135031-0003.flac"
text = ["THE LITTLE GIRL", "HAD BEEN ASLEEP", "BUT SHE HEARD THE RAPS", "AND OPENED THE DOOR"]
# align
segments = aligner(audio_path, text, name="1272-135031-0003")
print(segments)
# 1272-135031-0003_0000 1272-135031-0003 0.12 0.74 -0.0173 THE LITTLE GIRL
# 1272-135031-0003_0001 1272-135031-0003 0.74 1.54 -0.0012 HAD BEEN ASLEEP
# 1272-135031-0003_0002 1272-135031-0003 1.78 3.30 -0.0581 BUT SHE HEARD THE RAPS
# 1272-135031-0003_0003 1272-135031-0003 3.30 4.02 -0.0085 AND OPENED THE DOOR

Example 3 from WSJ:

# !wget https://github.com/espnet/espnet/blob/master/test_utils/ctc_align_test.wav
audio_path = "./ctc_align_test.wav"
text = ["THE SALE OF THE HOTELS", "IS PART OF HOLIDAY'S STRATEGY", "TO SELL OFF ASSETS", "AND CONCENTRATE", "ON PROPERTY MANAGEMENT"]
# align
segments = aligner(audio_path, text, name="ctc_align_test")
print(segments)
# ctc_align_test_0000 ctc_align_test 0.30 1.78 -0.0007 THE SALE OF THE HOTELS
# ctc_align_test_0001 ctc_align_test 1.78 3.19 -0.3872 IS PART OF HOLIDAY'S STRATEGY
# ctc_align_test_0002 ctc_align_test 3.19 4.21 -0.1871 TO SELL OFF ASSETS
# ctc_align_test_0003 ctc_align_test 4.21 4.87 -0.0001 AND CONCENTRATE
# ctc_align_test_0004 ctc_align_test 4.87 6.01 -0.0006 ON PROPERTY MANAGEMENT

Note that it is possible to filter out bad utterances based on their confidence score.

Text preprocessing

In general, ground truth text and audio should fit together as much as possible - which also depends on the dictionary of the ASR model.

For longer texts, it makes sense to partition it into sentences.

Special characters should be removed or transcribed to their equivalent in the model dicitionary, if available.

Numbers should be replaced with their spoken equivalents. If not done, the confidence score will be bad on all utterances with numbers. This can be done by number replacement with num2words:

from num2words import num2words
import re

# replace all the numbers
numbers = re.findall(r"\d+\.?\d*", utt_txt)
transcribed_numbers = [num2words(item, lang="fr") for item in numbers]

This however, does not take languages with flectation into account. In these cases, NLP taggers such as spacy help to determine the correct pronounciation.

Timings

Timings are estimated from the ratio of audio input samples to CTC-output indices (frames):

samples_to_frames_ratio = speech_len / lpz_len
index_duration = samples_to_frames_ratio / sample_rate
timing[index] = index * index_duration

For the English model above, the samples_to_frames_ratio is 640 and the index_duration is estimated to roughly 0.0400.

Timings are more accurate when pre-setting the correct timing information. This can be done if the samples-to-frames-ratio is already known; e.g., for the French model above:

aligner = CTCSegmentation(asr_model, kaldi_style_text=False, samples_to_frames_ratio=320, time_stamps="fixed")

How accurately the timings were estimated can be seen from the output object, e.g., segments.config.index_duration_in_seconds. Here with a Chinese model:

asr_model = EncoderDecoderASR.from_hparams(source="speechbrain/asr-wav2vec2-transformer-aishell")
aligner = CTCSegmentation(asr_model, kaldi_style_text=False
text = ["住房用地供应","和管理情况以及税收政策执行", "和征管情况"]
wav = "AISHELL-1_sample/S0150/S0150_mic/BAC009S0150W0240.wav"
segments = aligner(wav, text)
print(segments)
# utt_0000 utt 0.26 2.55 -0.6748 住房用地供应
# utt_0001 utt 2.55 6.67 -0.9303 和管理情况以及税收政策执行
# utt_0002 utt 6.67 8.53 -0.5613 和征管情况

# - get time resolution:
print(segments.config.index_duration_in_seconds)
# 0.020011077680525166

Handling longer files

CTC segmentation alignes an audio file of 3h in roughly 500ms, but the inference takes in many cases longer.

At inference time, the memory RNN models grow approximately linearly with length of audio file, while with Transformers, the memory requirement grows quadratrically. This example of an 8 minute audio file on a French model takes 80 GB RAM:

+ Code

Here we align the first chapter of an audio book "Les Miserables": With lesmiserables_t1_01_hugo.mp3 downloaded from https://archive.org/details/les_miserables_tome_1_di_0910_librivox .

audio = "/xxx/lesmiserables_t1_01_hugo.mp3"
text= """
Tome Un FANTINE.
Livre premier. Un juste.
Chapitre Un.
Monsieur Myriel.

En 1815, M. Charles-François-Bienvenu Myriel était évêque de Digne.
C'était un vieillard d'environ soixante-quinze ans; il occupait le siège
de Digne depuis 1806.

Quoique ce détail ne touche en aucune manière au fond même de ce que
nous avons à raconter, il n'est peut-être pas inutile, ne fût-ce que
pour être exact en tout, d'indiquer ici les bruits et les propos qui
avaient couru sur son compte au moment où il était arrivé dans le
diocèse. Vrai ou faux, ce qu'on dit des hommes tient souvent autant de
place dans leur vie et surtout dans leur destinée que ce qu'ils font. M.
Myriel était fils d'un conseiller au parlement d'Aix; noblesse de robe.
On contait de lui que son père, le réservant pour hériter de sa charge,
l'avait marié de fort bonne heure, à dix-huit ou vingt ans, suivant un
usage assez répandu dans les familles parlementaires. Charles Myriel,
nonobstant ce mariage, avait, disait-on, beaucoup fait parler de lui. Il
était bien fait de sa personne, quoique d'assez petite taille, élégant,
gracieux, spirituel; toute la première partie de sa vie avait été donnée
au monde et aux galanteries. La révolution survint, les événements se
précipitèrent, les familles parlementaires décimées, chassées, traquées,
se dispersèrent. M. Charles Myriel, dès les premiers jours de la
révolution, émigra en Italie. Sa femme y mourut d'une maladie de
poitrine dont elle était atteinte depuis longtemps. Ils n'avaient point
d'enfants. Que se passa-t-il ensuite dans la destinée de M. Myriel?
L'écroulement de l'ancienne société française, la chute de sa propre
famille, les tragiques spectacles de 93, plus effrayants encore
peut-être pour les émigrés qui les voyaient de loin avec le
grossissement de l'épouvante, firent-ils germer en lui des idées de
renoncement et de solitude? Fut-il, au milieu d'une de ces distractions
et de ces affections qui occupaient sa vie, subitement atteint d'un de
ces coups mystérieux et terribles qui viennent quelquefois renverser, en
le frappant au coeur, l'homme que les catastrophes publiques
n'ébranleraient pas en le frappant dans son existence et dans sa
fortune? Nul n'aurait pu le dire; tout ce qu'on savait, c'est que,
lorsqu'il revint d'Italie, il était prêtre.

En 1804, M. Myriel était curé de Brignolles. Il était déjà vieux, et
vivait dans une retraite profonde.

Vers l'époque du couronnement, une petite affaire de sa cure, on ne sait
plus trop quoi, l'amena à Paris. Entre autres personnes puissantes, il
alla solliciter pour ses paroissiens M. le cardinal Fesch. Un jour que
l'empereur était venu faire visite à son oncle, le digne curé, qui
attendait dans l'antichambre, se trouva sur le passage de sa majesté.
Napoléon, se voyant regardé avec une certaine curiosité par ce
vieillard, se retourna, et dit brusquement:

--Quel est ce bonhomme qui me regarde?

--Sire, dit M. Myriel, vous regardez un bonhomme, et moi je regarde un
grand homme. Chacun de nous peut profiter.

L'empereur, le soir même, demanda au cardinal le nom de ce curé, et
quelque temps après M. Myriel fut tout surpris d'apprendre qu'il était
nommé évêque de Digne.

Qu'y avait-il de vrai, du reste, dans les récits qu'on faisait sur la
première partie de la vie de M. Myriel? Personne ne le savait. Peu de
familles avaient connu la famille Myriel avant la révolution.

M. Myriel devait subir le sort de tout nouveau venu dans une petite
ville où il y a beaucoup de bouches qui parlent et fort peu de têtes qui
pensent. Il devait le subir, quoiqu'il fût évêque et parce qu'il était
évêque. Mais, après tout, les propos auxquels on mêlait son nom
n'étaient peut-être que des propos; du bruit, des mots, des paroles;
moins que des paroles, des _palabres_, comme dit l'énergique langue du
midi.

Quoi qu'il en fût, après neuf ans d'épiscopat et de résidence à Digne,
tous ces racontages, sujets de conversation qui occupent dans le premier
moment les petites villes et les petites gens, étaient tombés dans un
oubli profond. Personne n'eût osé en parler, personne n'eût même osé
s'en souvenir.

M. Myriel était arrivé à Digne accompagné d'une vieille fille,
mademoiselle Baptistine, qui était sa soeur et qui avait dix ans de
moins que lui.

Ils avaient pour tout domestique une servante du même âge que
mademoiselle Baptistine, et appelée madame Magloire, laquelle, après
avoir été _la servante de M. le Curé_, prenait maintenant le double
titre de femme de chambre de mademoiselle et femme de charge de
monseigneur.

Mademoiselle Baptistine était une personne longue, pâle, mince, douce;
elle réalisait l'idéal de ce qu'exprime le mot «respectable»; car il
semble qu'il soit nécessaire qu'une femme soit mère pour être vénérable.
Elle n'avait jamais été jolie; toute sa vie, qui n'avait été qu'une
suite de saintes oeuvres, avait fini par mettre sur elle une sorte de
blancheur et de clarté; et, en vieillissant, elle avait gagné ce qu'on
pourrait appeler la beauté de la bonté. Ce qui avait été de la maigreur
dans sa jeunesse était devenu, dans sa maturité, de la transparence; et
cette diaphanéité laissait voir l'ange. C'était une âme plus encore que
ce n'était une vierge. Sa personne semblait faite d'ombre; à peine assez
de corps pour qu'il y eût là un sexe; un peu de matière contenant une
lueur; de grands yeux toujours baissés; un prétexte pour qu'une âme
reste sur la terre.

Madame Magloire était une petite vieille, blanche, grasse, replète,
affairée, toujours haletante, à cause de son activité d'abord, ensuite à
cause d'un asthme.

À son arrivée, on installa M. Myriel en son palais épiscopal avec les
honneurs voulus par les décrets impériaux qui classent l'évêque
immédiatement après le maréchal de camp. Le maire et le président lui
firent la première visite, et lui de son côté fit la première visite au
général et au préfet.

L'installation terminée, la ville attendit son évêque à l'oeuvre"""

Then, apply text preprocessing to be close to the spoken equivalent:

# text preprocessing
text = text.replace("M.", "Monsieur")
text = text.replace("1815", "dix-huit cent quinze")
text = text.replace("1806", "dix-huit cent six")
text = text.replace("?", ".").replace(";", ".").replace(",", ".").replace("--", " ")
text = " ".join(text.split())

Split into sentences and align:

text = text.replace(". ", ".").upper().split(".")
# alignment
asr_model = EncoderASR.from_hparams(source="speechbrain/asr-wav2vec2-commonvoice-fr")
aligner = CTCSegmentation(asr_model, kaldi_style_text=False, gratis_blank=True)
segments = aligner(audio, text)
+ Segments

You may notice that the segments start only after 28s, that's because in the first part, the reader says book title, that's it's part of librivox, ...

utt_0000 utt 28.08 30.32 -0.5524 TOME UN FANTINE
utt_0001 utt 31.08 32.45 -0.1081 LIVRE PREMIER
utt_0002 utt 32.96 34.16 -0.1301 UN JUSTE
utt_0003 utt 34.70 35.98 -0.3609 CHAPITRE UN
utt_0004 utt 36.76 38.08 -0.3743 MONSIEUR MYRIEL
utt_0005 utt 39.98 41.61 -1.5222 EN DIX-HUIT CENT QUINZE
utt_0006 utt 41.61 45.89 -1.2631 MONSIEUR CHARLES-FRANÇOIS-BIENVENU MYRIEL ÉTAIT ÉVÊQUE DE DIGNE
utt_0007 utt 46.64 49.23 -0.7951 C'ÉTAIT UN VIEILLARD D'ENVIRON SOIXANTE-QUINZE ANS
utt_0008 utt 49.46 53.58 -1.0346 IL OCCUPAIT LE SIÈGE DE DIGNE DEPUIS DIX-HUIT CENT SIX
utt_0009 utt 54.50 59.75 -0.8542 QUOIQUE CE DÉTAIL NE TOUCHE EN AUCUNE MANIÈRE AU FOND MÊME DE CE QUE NOUS AVONS À RACONTER
utt_0010 utt 60.10 62.09 -0.5867 IL N'EST PEUT-ÊTRE PAS INUTILE
utt_0011 utt 62.22 64.55 -0.6493 NE FÛT-CE QUE POUR ÊTRE EXACT EN TOUT
utt_0012 utt 64.86 72.81 -0.4257 D'INDIQUER ICI LES BRUITS ET LES PROPOS QUI AVAIENT COURU SUR SON COMPTE AU MOMENT OÙ IL ÉTAIT ARRIVÉ DANS LE DIOCÈSE
utt_0013 utt 73.48 74.55 -0.2002 VRAI OU FAUX
utt_0014 utt 74.82 82.31 -0.5089 CE QU'ON DIT DES HOMMES TIENT SOUVENT AUTANT DE PLACE DANS LEUR VIE ET SURTOUT DANS LEUR DESTINÉE QUE CE QU'ILS FONT
utt_0015 utt 83.50 86.94 -0.9711 MONSIEUR MYRIEL ÉTAIT FILS D'UN CONSEILLER AU PARLEMENT D'AIX
utt_0016 utt 87.22 88.73 -0.3234 NOBLESSE DE ROBE
utt_0017 utt 89.82 91.87 -0.2654 ON CONTAIT DE LUI QUE SON PÈRE
utt_0018 utt 92.06 94.53 -0.3102 LE RÉSERVANT POUR HÉRITER DE SA CHARGE
utt_0019 utt 94.86 96.99 -0.9107 L'AVAIT MARIÉ DE FORT BONNE HEURE
utt_0020 utt 97.30 99.01 -1.0155 À DIX-HUIT OU VINGT ANS
utt_0021 utt 99.32 103.37 -0.3329 SUIVANT UN USAGE ASSEZ RÉPANDU DANS LES FAMILLES PARLEMENTAIRES
utt_0022 utt 104.20 105.46 -0.4780 CHARLES MYRIEL
utt_0023 utt 105.46 107.18 -1.1617 NONOBSTANT CE MARIAGE
utt_0024 utt 107.76 108.33 -0.0567 AVAIT
utt_0025 utt 108.33 109.05 -0.0309 DISAIT-ON
utt_0026 utt 109.24 110.81 -0.0517 BEAUCOUP FAIT PARLER DE LUI
utt_0027 utt 112.00 114.06 -0.1165 IL ÉTAIT BIEN FAIT DE SA PERSONNE
utt_0028 utt 114.18 115.95 -0.8009 QUOIQUE D'ASSEZ PETITE TAILLE
utt_0029 utt 116.32 117.18 -0.4313 ÉLÉGANT
utt_0030 utt 117.48 118.51 -0.6122 GRACIEUX
utt_0031 utt 118.64 119.71 -0.2498 SPIRITUEL
utt_0032 utt 120.66 125.85 -0.3959 TOUTE LA PREMIÈRE PARTIE DE SA VIE AVAIT ÉTÉ DONNÉE AU MONDE ET AUX GALANTERIES
utt_0033 utt 127.00 128.83 -0.4148 LA RÉVOLUTION SURVINT
utt_0034 utt 129.16 131.15 -0.6118 LES ÉVÉNEMENTS SE PRÉCIPITÈRENT
utt_0035 utt 131.40 133.83 -0.8563 LES FAMILLES PARLEMENTAIRES DÉCIMÉES
utt_0036 utt 133.83 134.75 -0.1775 CHASSÉES
utt_0037 utt 134.86 135.69 -0.2947 TRAQUÉES
utt_0038 utt 135.82 137.11 -0.2428 SE DISPERSÈRENT
utt_0039 utt 138.80 140.38 -0.3732 MONSIEUR CHARLES MYRIEL
utt_0040 utt 140.40 142.37 -0.4182 DÈS LES PREMIERS JOURS DE LA RÉVOLUTION
utt_0041 utt 142.88 144.35 -0.5594 ÉMIGRA EN ITALIE
utt_0042 utt 145.15 150.06 -0.3402 SA FEMME Y MOURUT D'UNE MALADIE DE POITRINE DONT ELLE ÉTAIT ATTEINTE DEPUIS LONGTEMPS
utt_0043 utt 150.55 152.04 -0.9263 ILS N'AVAIENT POINT D'ENFANTS
utt_0044 utt 152.55 156.22 -0.9884 QUE SE PASSA-T-IL ENSUITE DANS LA DESTINÉE DE MONSIEUR MYRIEL
utt_0045 utt 157.31 160.12 -0.3579 L'ÉCROULEMENT DE L'ANCIENNE SOCIÉTÉ FRANÇAISE
utt_0046 utt 160.37 162.52 -0.6507 LA CHUTE DE SA PROPRE FAMILLE
utt_0047 utt 162.77 165.66 -0.4019 LES TRAGIQUES SPECTACLES DE 93
utt_0048 utt 166.09 172.50 -0.7227 PLUS EFFRAYANTS ENCORE PEUT-ÊTRE POUR LES ÉMIGRÉS QUI LES VOYAIENT DE LOIN AVEC LE GROSSISSEMENT DE L'ÉPOUVANTE
utt_0049 utt 172.91 177.09 -1.6390 FIRENT-ILS GERMER EN LUI DES IDÉES DE RENONCEMENT ET DE SOLITUDE
utt_0050 utt 177.91 178.68 -0.3153 FUT-IL
utt_0051 utt 178.68 183.02 -0.7917 AU MILIEU D'UNE DE CES DISTRACTIONS ET DE CES AFFECTIONS QUI OCCUPAIENT SA VIE
utt_0052 utt 183.23 189.56 -1.3320 SUBITEMENT ATTEINT D'UN DE CES COUPS MYSTÉRIEUX ET TERRIBLES QUI VIENNENT QUELQUEFOIS RENVERSER
utt_0053 utt 189.59 191.00 -0.8259 EN LE FRAPPANT AU COEUR
utt_0054 utt 191.33 198.15 -0.6863 L'HOMME QUE LES CATASTROPHES PUBLIQUES N'ÉBRANLERAIENT PAS EN LE FRAPPANT DANS SON EXISTENCE ET DANS SA FORTUNE
utt_0055 utt 198.83 200.30 -0.2961 NUL N'AURAIT PU LE DIRE
utt_0056 utt 200.99 202.28 -0.1125 TOUT CE QU'ON SAVAIT
utt_0057 utt 202.33 203.00 -0.2763 C'EST QUE
utt_0058 utt 203.00 204.34 -0.6415 LORSQU'IL REVINT D'ITALIE
utt_0059 utt 204.34 205.50 -0.2598 IL ÉTAIT PRÊTRE
utt_0060 utt 206.25 206.90 -0.1614 EN 1804
utt_0061 utt 207.97 210.28 -0.4308 MONSIEUR MYRIEL ÉTAIT CURÉ DE BRIGNOLLES
utt_0062 utt 210.69 212.09 -0.2811 IL ÉTAIT DÉJÀ VIEUX
utt_0063 utt 212.09 214.58 -0.3625 ET VIVAIT DANS UNE RETRAITE PROFONDE
utt_0064 utt 215.57 217.34 -0.1666 VERS L'ÉPOQUE DU COURONNEMENT
utt_0065 utt 217.34 219.16 -0.3601 UNE PETITE AFFAIRE DE SA CURE
utt_0066 utt 219.45 220.82 -0.0968 ON NE SAIT PLUS TROP QUOI
utt_0067 utt 221.09 222.41 -0.3949 L'AMENA À PARIS
utt_0068 utt 223.13 225.04 -0.4874 ENTRE AUTRES PERSONNES PUISSANTES
utt_0069 utt 225.31 229.80 -0.5230 IL ALLA SOLLICITER POUR SES PAROISSIENS MONSIEUR LE CARDINAL FESCH
utt_0070 utt 230.61 234.30 -0.5074 UN JOUR QUE L'EMPEREUR ÉTAIT VENU FAIRE VISITE À SON ONCLE
utt_0071 utt 234.57 235.83 -0.3311 LE DIGNE CURÉ
utt_0072 utt 235.87 237.66 -0.3142 QUI ATTENDAIT DANS L'ANTICHAMBRE
utt_0073 utt 237.87 240.58 -0.3032 SE TROUVA SUR LE PASSAGE DE SA MAJESTÉ
utt_0074 utt 241.31 242.22 -0.3510 NAPOLÉON
utt_0075 utt 242.45 246.18 -0.2590 SE VOYANT REGARDÉ AVEC UNE CERTAINE CURIOSITÉ PAR CE VIEILLARD
utt_0076 utt 246.55 247.51 -0.3290 SE RETOURNA
utt_0077 utt 247.51 251.43 -1.9178 ET DIT BRUSQUEMENT: QUEL EST CE BONHOMME QUI ME REGARDE
utt_0078 utt 252.29 252.84 -0.5249 SIRE
utt_0079 utt 253.01 254.30 -0.3924 DIT MONSIEUR MYRIEL
utt_0080 utt 254.69 256.46 -0.5346 VOUS REGARDEZ UN BONHOMME
utt_0081 utt 256.75 258.86 -0.4982 ET MOI JE REGARDE UN GRAND HOMME
utt_0082 utt 259.23 261.37 -0.2903 CHACUN DE NOUS PEUT PROFITER
utt_0083 utt 262.63 263.54 -0.4029 L'EMPEREUR
utt_0084 utt 263.54 264.50 -0.2678 LE SOIR MÊME
utt_0085 utt 264.81 267.58 -0.5795 DEMANDA AU CARDINAL LE NOM DE CE CURÉ
utt_0086 utt 267.69 273.51 -0.6112 ET QUELQUE TEMPS APRÈS MONSIEUR MYRIEL FUT TOUT SURPRIS D'APPRENDRE QU'IL ÉTAIT NOMMÉ ÉVÊQUE DE DIGNE
utt_0087 utt 275.59 277.07 -0.9276 QU'Y AVAIT-IL DE VRAI
utt_0088 utt 277.07 277.80 -0.2477 DU RESTE
utt_0089 utt 277.95 282.19 -0.9001 DANS LES RÉCITS QU'ON FAISAIT SUR LA PREMIÈRE PARTIE DE LA VIE DE MONSIEUR MYRIEL
utt_0090 utt 282.77 284.34 -0.0757 PERSONNE NE LE SAVAIT
utt_0091 utt 284.81 288.32 -1.0121 PEU DE FAMILLES AVAIENT CONNU LA FAMILLE MYRIEL AVANT LA RÉVOLUTION
utt_0092 utt 289.27 298.66 -0.7310 MONSIEUR MYRIEL DEVAIT SUBIR LE SORT DE TOUT NOUVEAU VENU DANS UNE PETITE VILLE OÙ IL Y A BEAUCOUP DE BOUCHES QUI PARLENT ET FORT PEU DE TÊTES QUI PENSENT
utt_0093 utt 299.41 300.72 -0.0832 IL DEVAIT LE SUBIR
utt_0094 utt 300.72 303.62 -0.2762 QUOIQU'IL FÛT ÉVÊQUE ET PARCE QU'IL ÉTAIT ÉVÊQUE
utt_0095 utt 304.91 305.42 -0.0168 MAIS
utt_0096 utt 305.45 306.36 -0.1922 APRÈS TOUT
utt_0097 utt 306.47 311.39 -0.8061 LES PROPOS AUXQUELS ON MÊLAIT SON NOM N'ÉTAIENT PEUT-ÊTRE QUE DES PROPOS
utt_0098 utt 311.75 312.61 -0.2856 DU BRUIT
utt_0099 utt 312.75 313.52 -0.1589 DES MOTS
utt_0100 utt 313.59 314.54 -0.0826 DES PAROLES
utt_0101 utt 314.95 316.22 -0.0909 MOINS QUE DES PAROLES
utt_0102 utt 316.25 317.44 -0.4752 DES _PALABRES_
utt_0103 utt 317.47 319.87 -0.1557 COMME DIT L'ÉNERGIQUE LANGUE DU MIDI
utt_0104 utt 320.77 321.96 -0.4920 QUOI QU'IL EN FÛT
utt_0105 utt 322.07 325.69 -1.5119 APRÈS NEUF ANS D'ÉPISCOPAT ET DE RÉSIDENCE À DIGNE
utt_0106 utt 325.97 327.46 -0.1400 TOUS CES RACONTAGES
utt_0107 utt 327.55 333.60 -0.8017 SUJETS DE CONVERSATION QUI OCCUPENT DANS LE PREMIER MOMENT LES PETITES VILLES ET LES PETITES GENS
utt_0108 utt 333.77 336.05 -0.6689 ÉTAIENT TOMBÉS DANS UN OUBLI PROFOND
utt_0109 utt 336.71 338.59 -1.3516 PERSONNE N'EÛT OSÉ EN PARLER
utt_0110 utt 338.73 341.26 -1.1054 PERSONNE N'EÛT MÊME OSÉ S'EN SOUVENIR
utt_0111 utt 342.97 347.09 -0.1635 MONSIEUR MYRIEL ÉTAIT ARRIVÉ À DIGNE ACCOMPAGNÉ D'UNE VIEILLE FILLE
utt_0112 utt 347.09 348.64 -0.1329 MADEMOISELLE BAPTISTINE
utt_0113 utt 348.83 352.08 -0.2481 QUI ÉTAIT SA SOEUR ET QUI AVAIT DIX ANS DE MOINS QUE LUI
utt_0114 utt 353.27 358.38 -0.4945 ILS AVAIENT POUR TOUT DOMESTIQUE UNE SERVANTE DU MÊME ÂGE QUE MADEMOISELLE BAPTISTINE
utt_0115 utt 358.49 360.23 -0.2872 ET APPELÉE MADAME MAGLOIRE
utt_0116 utt 360.63 361.43 -0.2986 LAQUELLE
utt_0117 utt 361.69 364.74 -0.6303 APRÈS AVOIR ÉTÉ _LA SERVANTE DE MONSIEUR LE CURÉ_
utt_0118 utt 364.99 372.23 -0.2863 PRENAIT MAINTENANT LE DOUBLE TITRE DE FEMME DE CHAMBRE DE MADEMOISELLE ET FEMME DE CHARGE DE MONSEIGNEUR
utt_0119 utt 373.63 376.70 -0.3932 MADEMOISELLE BAPTISTINE ÉTAIT UNE PERSONNE LONGUE
utt_0120 utt 376.99 377.70 -0.0751 PÂLE
utt_0121 utt 378.03 378.66 -0.3890 MINCE
utt_0122 utt 378.99 379.60 -0.2301 DOUCE
utt_0123 utt 380.39 385.24 -0.6638 ELLE RÉALISAIT L'IDÉAL DE CE QU'EXPRIME LE MOT «RESPECTABLE»
utt_0124 utt 385.81 391.29 -0.4178 CAR IL SEMBLE QU'IL SOIT NÉCESSAIRE QU'UNE FEMME SOIT MÈRE POUR ÊTRE VÉNÉRABLE
utt_0125 utt 391.75 393.48 -0.3714 ELLE N'AVAIT JAMAIS ÉTÉ JOLIE
utt_0126 utt 393.53 394.60 -0.1253 TOUTE SA VIE
utt_0127 utt 394.69 397.88 -1.1759 QUI N'AVAIT ÉTÉ QU'UNE SUITE DE SAINTES OEUVRES
utt_0128 utt 398.07 402.56 -0.4144 AVAIT FINI PAR METTRE SUR ELLE UNE SORTE DE BLANCHEUR ET DE CLARTÉ
utt_0129 utt 403.15 403.66 -0.0811 ET
utt_0130 utt 403.66 404.41 -0.2271 EN VIEILLISSANT
utt_0131 utt 404.55 409.00 -0.5422 ELLE AVAIT GAGNÉ CE QU'ON POURRAIT APPELER LA BEAUTÉ DE LA BONTÉ
utt_0132 utt 410.45 414.64 -0.2964 CE QUI AVAIT ÉTÉ DE LA MAIGREUR DANS SA JEUNESSE ÉTAIT DEVENU
utt_0133 utt 414.64 415.63 -0.0525 DANS SA MATURITÉ
utt_0134 utt 415.83 417.24 -0.1909 DE LA TRANSPARENCE
utt_0135 utt 418.11 422.44 -0.5773 ET CETTE DIAPHANÉITÉ LAISSAIT VOIR L'ANGE
utt_0136 utt 423.39 426.82 -0.5336 C'ÉTAIT UNE ÂME PLUS ENCORE QUE CE N'ÉTAIT UNE VIERGE
utt_0137 utt 427.57 430.10 -0.2424 SA PERSONNE SEMBLAIT FAITE D'OMBRE
utt_0138 utt 430.43 433.56 -0.7741 À PEINE ASSEZ DE CORPS POUR QU'IL Y EÛT LÀ UN SEXE
utt_0139 utt 433.68 436.17 -0.0782 UN PEU DE MATIÈRE CONTENANT UNE LUEUR
utt_0140 utt 436.42 438.47 -0.5383 DE GRANDS YEUX TOUJOURS BAISSÉS
utt_0141 utt 438.52 441.53 -0.9977 UN PRÉTEXTE POUR QU'UNE ÂME RESTE SUR LA TERRE
utt_0142 utt 443.08 445.61 -0.0723 MADAME MAGLOIRE ÉTAIT UNE PETITE VIEILLE
utt_0143 utt 445.88 446.63 -0.1467 BLANCHE
utt_0144 utt 446.86 447.65 -0.8112 GRASSE
utt_0145 utt 447.90 448.71 -0.1460 REPLÈTE
utt_0146 utt 448.92 449.79 -0.6227 AFFAIRÉE
utt_0147 utt 450.00 451.49 -0.3090 TOUJOURS HALETANTE
utt_0148 utt 451.62 453.71 -0.5239 À CAUSE DE SON ACTIVITÉ D'ABORD
utt_0149 utt 453.94 456.09 -0.4499 ENSUITE À CAUSE D'UN ASTHME
utt_0150 utt 458.38 459.47 -0.0230 À SON ARRIVÉE
utt_0151 utt 459.58 469.93 -1.2239 ON INSTALLA MONSIEUR MYRIEL EN SON PALAIS ÉPISCOPAL AVEC LES HONNEURS VOULUS PAR LES DÉCRETS IMPÉRIAUX QUI CLASSENT L'ÉVÊQUE IMMÉDIATEMENT APRÈS LE MARÉCHAL DE CAMP
utt_0152 utt 470.84 474.33 -0.3690 LE MAIRE ET LE PRÉSIDENT LUI FIRENT LA PREMIÈRE VISITE
utt_0153 utt 474.78 479.51 -1.0417 ET LUI DE SON CÔTÉ FIT LA PREMIÈRE VISITE AU GÉNÉRAL ET AU PRÉFET
utt_0154 utt 480.34 481.91 -0.3336 L'INSTALLATION TERMINÉE
utt_0155 utt 482.18 484.47 -1.0307 LA VILLE ATTENDIT SON ÉVÊQUE À L'OEUVRE

Therefore in many cases, it's practical to partition long audio file into shorter parts.

+ Audio partitioning

An example of how to do this was proposed by Takamichi et al. in JTUBESPEECH: CORPUS OF JAPANESE SPEECH COLLECTED FROM YOUTUBE FOR SPEECH RECOGNITION AND SPEAKER VERIFICATION. As the CTC activations are distorted at the partition borders, choose slightly overlapping partitions and delete the additional CTC output indices:

def get_partitions(
    t: int = 100000,
    max_len_s: float = 1280.0,
    fs: int = 16000,
    samples_to_frames_ratio=512,
    overlap: int = 0,
):
    """Obtain partitions
    Note that this is implemented for frontends that discard trailing data.
    Note that the partitioning strongly depends on your architecture.
    A note on audio indices:
        Based on the ratio of audio sample points to lpz indices (here called
        frame), the start index of block N is:
        0 + N * samples_to_frames_ratio
        Due to the discarded trailing data, the end is then in the range of:
        [N * samples_to_frames_ratio - 1 .. (1+N) * samples_to_frames_ratio] ???
    """
    # max length should be ~ cut length + 25%
    cut_time_s = max_len_s / 1.25
    max_length = int(max_len_s * fs)
    cut_length = int(cut_time_s * fs)
    # make sure its a multiple of frame size
    max_length -= max_length % samples_to_frames_ratio
    cut_length -= cut_length % samples_to_frames_ratio
    overlap = int(max(0, overlap))
    if (max_length - cut_length) <= samples_to_frames_ratio * (2 + overlap):
        raise ValueError(
            f"Pick a larger time value for partitions. "
            f"time value: {max_len_s}, "
            f"overlap: {overlap}, "
            f"ratio: {samples_to_frames_ratio}."
        )
    partitions = []
    duplicate_frames = []
    cumulative_lpz_length = 0
    cut_length_lpz_frames = int(cut_length // samples_to_frames_ratio)
    partition_start = 0
    while t > max_length:
        start = int(max(0, partition_start - samples_to_frames_ratio * overlap))
        end = int(
            partition_start + cut_length + samples_to_frames_ratio * (1 + overlap) - 1
        )
        partitions += [(start, end)]
        # overlap - duplicate frames shall be deleted.
        cumulative_lpz_length += cut_length_lpz_frames
        for i in range(overlap):
            duplicate_frames += [
                cumulative_lpz_length - i,
                cumulative_lpz_length + (1 + i),
            ]
        # next partition
        t -= cut_length
        partition_start += cut_length
    else:
        start = int(max(0, partition_start - samples_to_frames_ratio * overlap))
        partitions += [(start, None)]
    partition_dict = {
        "partitions": partitions,
        "overlap": overlap,
        "delete_overlap_list": duplicate_frames,
        "samples_to_frames_ratio": samples_to_frames_ratio,
        "max_length": max_length,
        "cut_length": cut_length,
        "cut_time_s": cut_time_s,
    }
    return partition_dict

# infer
lpzs = [
    torch.tensor(aligner.get_lpz(speech[start:end]))
    for start, end in partitions["partitions"]
]
# concatenate audio vectors
lpz = torch.cat(lpzs).numpy()
# delete the overlaps
lpz = np.delete(lpz, partitions["delete_overlap_list"], axis=0)

Source: https://github.com/sarulab-speech/jtubespeech/blob/053935c41fd7452ef073893227e8760f31f3884e/scripts/align.py#L89-L157

Multiprocessing

For processing large corpora, it is practical to parallelize computations as much as possible. Inference and alignment can be done in two separate steps:

# inference
lpz = aligner.get_lpz(speech)
# prepare as a task object
task = aligner.prepare_segmentation_task(text, lpz, name=audio_name, speech_len=lpz.shape[0])
# align
result = CTCSegmentation.get_segments(task)
task.set(**result)

The task can be handled by a multiprocessing worker. Tasks can be

import soundfile
from torch.multiprocessing import Process, Queue

def align_worker(in_queue, out_queue, num=0):
    print(f"align_worker {num} started")
    for task in iter(in_queue.get, "STOP"):
        result = CTCSegmentation.get_segments(task)
        task.set(**result)
        print(task)
        del task
    print(f"align_worker {num} stopped")


# start queue
NUMBER_OF_PROCESSES = 4
task_queue = Queue(maxsize=NUMBER_OF_PROCESSES)
# inference - iterate over audio files
for text, audio_file in audio_file_list:
    speech, sample_rate = soundfile.read(audio_file)
    lpz = aligner.get_lpz(speech)
    audio_name = Path(audio_file).stem
    task = aligner.prepare_segmentation_task(text, lpz, name=audio_name, speech_len=lpz.shape[0])
    task_queue.put(task)
# finish by telling child processes to stop
for i in range(NUMBER_OF_PROCESSES):
    task_queue.put("STOP")
@snami100
Copy link

snami100 commented Nov 3, 2021

is this also possible for german language?

@lumaku
Copy link
Author

lumaku commented Nov 4, 2021

I don't know of any German models for Speechbrain; there are currently only models for ESPnet 2 (hui_acg) and for wav2vec2
(facebook/wav2vec2-large-xlsr-53-german). CTC segmentation is also integrated in Espnet 2 and the CTC segmentation README contains an example for wav2vec2.

@ralonila
Copy link

Thank you for this code and wonderful contribution . I run the first short example and received the results that you report. but opening the audio file and checking the alignment I found that it is wrong i.e - in the first segment the last word is missing, in the second segment the last word is cut and most of it appear in the third segment. The audio is very clean and there are spaces between the words so I do not understand why this is the result.
Can you please advice

@lumaku
Copy link
Author

lumaku commented Jan 26, 2022

@ralonila
From my experience with the algorithm, the issue is two-fold:

  1. As the CTC output only gives the time steps of occurrence of each token, we only can guess the timings from the time stamp between the tokens. The estimation for the last token is often shifted because the following token is a blank that usually occurs in the time stamp directly after it (25 ms). Therefore, it is hard to estimate the real ending of the last token...
  2. The CTC activations are also often shifted, sometimes by up to 300 ms. This is a phenomenon of the network and depends on time resolution and used network architecture. This might be the case in the middle of the audio file - but these distortions happen especially within 600ms-1s at the beginning and the end of the audio.

To solve this: Make sure your audio does not cut too early. If this issue persists, it is a good approximation to add a few ms to the ending of the last timing, depending on the average duration of a token in your ASR system. If your audio is clean, you can solve it similar to Bakhturina et al., who used an algorithm for voice activity detection for the last audio segment.

@Ca-ressemble-a-du-fake
Copy link

Thanks for this tutorial and explanations. I want to try it but I get "The given asr_model has no CTC module!" when running aligner

asr_model = EncoderDecoderASR.from_hparams(source="speechbrain/asr-wav2vec2-commonvoice-fr",  run_opts={"device":device} )

aligner = CTCSegmentation(asr_model, kaldi_style_text=False, gratis_blank=True)

I checked on the model web page and the model has well been trained with CTC, so what can I do to actually run the tutorial ?

Thanks for your help

@lumaku
Copy link
Author

lumaku commented Jan 9, 2023

Hey @Ca-ressemble-a-du-fake! This seems to be a model-specific issue. I opened a Speechbrain issue for this at speechbrain/speechbrain#1796

@Ca-ressemble-a-du-fake
Copy link

Great that you reported it @lumaku . I will try what @TParcollet suggests (use EncoderASR instead of EncoderDecoderASR)!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment