File size: 3,406 Bytes
79cf5f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import pathlib

import click
import textgrid
import tqdm


@click.command(help='Align words tiers in TextGrids to phones tiers')
@click.option('--tg', required=True, help='Path to TextGrids (2-tier or 3-tier format)')
@click.option('--dictionary', required=True, help='Path to the dictionary file')
@click.option(
    '--out', required=False,
    help='Path to save the aligned TextGrids (defaults to the input directory)'
)
@click.option('--overwrite', is_flag=True, help='Overwrite existing files')
def align_tg_words(tg, dictionary, out, overwrite):
    tg_path_in = pathlib.Path(tg)
    dict_path = pathlib.Path(dictionary)
    tg_path_out = pathlib.Path(out) if out is not None else tg_path_in
    tg_path_out.mkdir(parents=True, exist_ok=True)

    with open(dict_path, 'r', encoding='utf8') as f:
        rules = [ln.strip().split('\t') for ln in f.readlines()]
    dictionary = {
        'SP': ['SP'],
        'AP': ['AP']
    }
    phoneme_set = {'SP', 'AP'}
    for r in rules:
        phonemes = r[1].split()
        dictionary[r[0]] = phonemes
        phoneme_set.update(phonemes)

    for tgfile in tqdm.tqdm(tg_path_in.glob('*.TextGrid')):
        tg = textgrid.TextGrid()
        tg.read(tgfile)
        old_words_tier: textgrid.IntervalTier = tg[-2]
        if old_words_tier.name != 'words':
            raise ValueError(
                f'Invalid tier name or order in \'{tgfile}\'. '
                f'The words tier should be the 1st tier of a 2-tier TextGrid, '
                f'or the 2nd tier of a 3-tier TextGrid.'
            )
        phones_tier: textgrid.IntervalTier = tg[-1]
        new_words_tier = textgrid.IntervalTier(name='words')
        word_seq = [i.mark for i in old_words_tier]
        word_div = []
        ph_seq = [i.mark for i in phones_tier]
        ph_dur = [i.duration() for i in phones_tier]
        idx = 0
        for i, word in enumerate(word_seq):
            if word not in dictionary:
                raise ValueError(f'Error invalid word in \'{tgfile}\' at {i}: {word}')
            word_ph_seq = dictionary[word]
            ph_num = len(word_ph_seq)
            word_div.append(ph_num)
            if word_ph_seq != ph_seq[idx: idx + ph_num]:
                print(
                    f'Warning: word and phones mismatch in \'{tgfile}\' '
                    f'at word {i}, phone {idx}: {word} => {ph_seq[idx: idx + len(word_ph_seq)]}'
                )
            idx += ph_num
        for i, phone in enumerate(ph_seq):
            if phone not in phoneme_set:
                raise ValueError(f'Error: invalid phone in \'{tgfile}\' at {i}: {phone}')
        if sum(word_div) != len(ph_dur):
            raise ValueError(
                f'Error: word_div does not sum to number of phones in \'{tgfile}\'. '
                f'Check the warnings above for more detailed mismatching positions.'
            )
        start = 0.
        idx = 0
        for j in range(len(word_seq)):
            end = start + sum(ph_dur[idx: idx + word_div[j]])
            new_words_tier.add(minTime=start, maxTime=end, mark=word_seq[j])
            start = end
            idx += word_div[j]
        tg.tiers[-2] = new_words_tier
        tg_file_out = tg_path_out / tgfile.name
        if tg_file_out.exists() and not overwrite:
            raise FileExistsError(str(tg_file_out))
        tg.write(tg_file_out)


if __name__ == '__main__':
    align_tg_words()