실험음성학 강독회 2019년 9월 18일
성신여자대학교 영어영문학과
윤태진 교수

실험음성학 강독회

Tutorial: Python Package들을 이용한 음성 및 언어 자질 추출

1. 실험음성학 강독회 Tutorial 개요

실험음성학 2019학년도 2학기 첫 강독회에서는 Praat가 가진 음향 자질 추출의 장점과 Python이 가진 텍스트 자질 추출의 장점을 함께 활용해 보는 시간을 가질 것입니다. Python을 활용하여 Praat의 여러 기능들을 통제할 수 있으면, Praat이 음성 분석에서 가지는 장점은 물론 범용 프로그래밍 언어어로서의 Python이 가지는 여러 장점도 함께 활용할 수 있어 아주 강력한 파워를 가질 수 있게 됩니다. 따라서 본 강독회에서는 Python에서 Praat을 조종할 수 있는 Parselmouth라는 패키지와 Praat의 TextGrid에 있는 정보를 활용할 수 있는 tgt라는 모듈을 활용하여 음향 자질과 텍스트 자질을 통합하여 음성분석을 할 수 있는 기반에 대해 Jupyter Notebook을 기반으로 하여 배울 예정입니다.

Python을 사용해 왔고, pip 등을 통해 패키지 등을 설치해 본 경험이 있으면 사용하던 컴퓨터를 가지고 와 활용할 수 있을 것입니다. Python을 많이 사용해 보지 않았거나 처음인 분들은 https://www.anaconda.com/downloads에서 자신의 OS에 맞는 anaconda 배포판을 설치해서 강독회에 참석해 주시면 되겠습니다. 예를 들어 window용 노트북을 사용하는 분들은 anaconda에서 Python 3.7 version에서 64-bit Graphical Installer를 눌러 설치하면 됩니다.

2. Parselmouth & tgt

In [2]:
import parselmouth
import IPython.display
In [3]:
sound=parselmouth.Sound("data/SX127.wav")
In [4]:
from IPython.display import Audio
Audio(data=sound.values, rate=sound.sampling_frequency)
Out[4]:

3. Combining Parselmouth & tgt

In [5]:
import glob, re, tgt
for tg_file in glob.glob('data/*.TextGrid'):
    
    # 텍스트그리드 파일을 읽어들임
    print("processing {}...".format(tg_file))
    
    # 디렉토리와 파일명이 /로 결합되어 있는 데, 이를 분리하고, 파일명 앞까지 
    # 분리된 디렉토리들을 결합하여 디렉토리명으로 지칭함. 

    dir_name = tg_file.split('/')[:-1]
    dir_name = '/'.join(dir_name)
    print("directory name... {}".format(dir_name))
    
    # 마지막 [-1]로 된 것은 파일명이며, 텍스트그리드를 읽어들였으니 tg_name으로 명명함
    tg_name = tg_file.split('/')[-1]
    print('textgrid name... {}'.format(tg_name))
    
    # wave 파일과 TextGrid 파일은 이름을 공유하고 있으므로 extension만 wav로 바꾸고
    # wav_name으로 저장함. 
    wav_name = re.sub("TextGrid", "wav", tg_name)
    print("wave file name...{}".format(wav_name))
    
    # tgt를 이용해 textgrid 파일을 읽어들임.
    tg = tgt.read_textgrid(dir_name + '/' + tg_name)

    # tg의 파일명 알아보기
    tgname = tg.filename
    print("textgrid의 파일명...{}".format(tgname))
    
    #textgrid tier name 알아보기
    tg_tier_names = tg.get_tier_names()
    print("textgrid tier names...{}".format(tg_tier_names))
    
    # 찾아낸 tier명을 사용사여 tier들을 지정하기
    phn_tier = tg.get_tier_by_name('phn')
    wrd_tier = tg.get_tier_by_name('wrd')

    # 
    overlap_tier = tgt.util.get_overlapping_intervals(phn_tier, wrd_tier)
    #print('overlap_tier...{}'.format(overlap_tier))
    print('idx','\t','start','\t','end','\t','dur','\t','phone','\t','word')
    for i, t in enumerate(overlap_tier):
        #print(i, t)
        texts = t.text.split('+')
        duration = (t.end_time-t.start_time)*1000 # miliseconds
        print(format(i),'\t',
              format(t.start_time, ".3f"),'\t',
              format(t.end_time, ".3f"),'\t',
              format(duration, ".2f"),'\t',
              format(texts[0]),'\t',
              format(texts[1]))
processing data/SX127.TextGrid...
directory name... data
textgrid name... SX127.TextGrid
wave file name...SX127.wav
textgrid의 파일명...data/SX127.TextGrid
textgrid tier names...['phn', 'ipa', 'wrd']
idx 	 start 	 end 	 dur 	 phone 	 word
0 	 0.139 	 0.177 	 37.69 	 dh 	 the
1 	 0.177 	 0.235 	 57.69 	 iy 	 the
2 	 0.315 	 0.376 	 61.12 	 eh 	 emperor
3 	 0.376 	 0.427 	 50.13 	 m 	 emperor
4 	 0.427 	 0.442 	 15.31 	 pcl 	 emperor
5 	 0.442 	 0.497 	 55.00 	 p 	 emperor
6 	 0.497 	 0.543 	 46.19 	 r 	 emperor
7 	 0.543 	 0.577 	 33.94 	 ix 	 emperor
8 	 0.577 	 0.635 	 58.00 	 hv 	 had
9 	 0.635 	 0.728 	 92.50 	 ae 	 had
10 	 0.728 	 0.753 	 25.00 	 dx 	 had
11 	 0.753 	 0.785 	 32.50 	 ix 	 a
12 	 0.785 	 0.880 	 95.00 	 m 	 mean
13 	 0.880 	 0.975 	 95.00 	 iy 	 mean
14 	 0.975 	 1.045 	 70.06 	 n 	 mean
15 	 1.045 	 1.083 	 37.44 	 tcl 	 temper
16 	 1.083 	 1.149 	 66.25 	 t 	 temper
17 	 1.149 	 1.235 	 86.25 	 eh 	 temper
18 	 1.235 	 1.274 	 39.12 	 m 	 temper
19 	 1.274 	 1.313 	 39.00 	 pcl 	 temper
20 	 1.313 	 1.343 	 29.37 	 p 	 temper
21 	 1.343 	 1.417 	 75.00 	 axr 	 temper

4. Combining Parselmouth, tgt & Praat

In [6]:
from parselmouth.praat import call

import glob, re, tgt
for tg_file in glob.glob('data/*.TextGrid'):
    
    # 텍스트그리드 파일을 읽어들임
    print("processing {}...".format(tg_file))
    
    # 디렉토리와 파일명이 /로 결합되어 있는 데, 이를 분리하고, 파일명 앞까지 
    # 분리된 디렉토리들을 결합하여 디렉토리명으로 지칭함. 

    dir_name = tg_file.split('/')[:-1]
    dir_name = '/'.join(dir_name)
    print("directory name... {}".format(dir_name))
    
    # 마지막 [-1]로 된 것은 파일명이며, 텍스트그리드를 읽어들였으니 tg_name으로 명명함
    tg_name = tg_file.split('/')[-1]
    print('textgrid name... {}'.format(tg_name))
    
    # wave 파일과 TextGrid 파일은 이름을 공유하고 있으므로 extension만 wav로 바꾸고
    # wav_name으로 저장함. 
    wav_name = re.sub("TextGrid", "wav", tg_name)
    print("wave file name...{}".format(wav_name))
    
    # tgt를 이용해 textgrid 파일을 읽어들임.
    tg = tgt.read_textgrid(dir_name + '/' + tg_name)

    # tg의 파일명 알아보기
    tgname = tg.filename
    print("textgrid의 파일명...{}".format(tgname))
    
    #textgrid tier name 알아보기
    tg_tier_names = tg.get_tier_names()
    print("textgrid tier names...{}".format(tg_tier_names))
    
    # 찾아낸 tier명을 사용사여 tier들을 지정하기
    phn_tier = tg.get_tier_by_name('phn')
    wrd_tier = tg.get_tier_by_name('wrd')

    # 
    overlap_tier = tgt.util.get_overlapping_intervals(phn_tier, wrd_tier)
    
    ##################################################################
    # Parselmouth에세 Praat을 통제하여 음성 자질 추출하기
    ###################################################################
    
    wave_full_name = dir_name + '/' + wav_name
    
    # 소리
    sound = parselmouth.Sound(wave_full_name)
    
    # Praat을 이용한 Pitch analysis
    pitchobj = call(sound, "To Pitch (ac)", 0.0, 75, 15, False, 0.02, 0.45, 
                    0.01, 0.35, 0.14, 450) 
                    
    # Intensity Analysis
    intensityobj = call(sound, "To Intensity", 100, 0, True)
    
    # Formant
    formantobj = call(sound, "To Formant (burg)", 0, 5, 5500, 0.005, 50)
    
    #print('overlap_tier...{}'.format(overlap_tier))
    print('idx','\t','start','\t','end','\t','dur','\t','f0','\t','db','\t',
          'f1','\t','\f2','\t','\f3','\t',
          'phone','\t','word')
    for i, t in enumerate(overlap_tier):
        #print(i, t)
        texts = t.text.split('+')
        duration = (t.end_time-t.start_time)*1000 # miliseconds
        
        mid_time = t.start_time+(t.end_time-t.start_time)/2
        
        pitchvalue = call(pitchobj, "Get value at time", mid_time , "Hertz", "Linear")
        intensityvalue = call(intensityobj, "Get value at time", mid_time, "cubic")
        f1value = call(formantobj, "Get value at time", 1, mid_time, "Hertz", "Linear")
        f2value = call(formantobj, "Get value at time", 2, mid_time, "Hertz", "Linear")
        f3value = call(formantobj, "Get value at time", 3, mid_time, "Hertz", "Linear")
        
        print(format(i),'\t',
              format(t.start_time, ".3f"),'\t',
              format(t.end_time, ".3f"),'\t',
              format(duration, ".2f"),'\t',
              format(pitchvalue, ".2f"),'\t',
              format(intensityvalue, ".2f"),'\t',
              format(f1value, ".2f"),'\t',
              format(f2value, ".2f"),'\t',
              format(f3value, ".2f"),'\t',
              format(texts[0]),'\t',
              format(texts[1]))
processing data/SX127.TextGrid...
directory name... data
textgrid name... SX127.TextGrid
wave file name...SX127.wav
textgrid의 파일명...data/SX127.TextGrid
textgrid tier names...['phn', 'ipa', 'wrd']
idx 	 start 	 end 	 dur 	 f0 	 db 	 f1 	 2 	 3 	 phone 	 word
0 	 0.139 	 0.177 	 37.69 	 nan 	 43.03 	 93.65 	 2061.07 	 2829.39 	 dh 	 the
1 	 0.177 	 0.235 	 57.69 	 92.25 	 57.28 	 418.40 	 2326.78 	 2889.33 	 iy 	 the
2 	 0.315 	 0.376 	 61.12 	 210.43 	 59.57 	 810.20 	 1736.84 	 2293.68 	 eh 	 emperor
3 	 0.376 	 0.427 	 50.13 	 229.86 	 48.46 	 272.67 	 1307.27 	 2276.59 	 m 	 emperor
4 	 0.427 	 0.442 	 15.31 	 243.50 	 35.89 	 417.25 	 1565.05 	 2476.33 	 pcl 	 emperor
5 	 0.442 	 0.497 	 55.00 	 nan 	 34.64 	 1214.83 	 1704.56 	 2919.78 	 p 	 emperor
6 	 0.497 	 0.543 	 46.19 	 234.52 	 54.95 	 478.76 	 1216.00 	 1885.12 	 r 	 emperor
7 	 0.543 	 0.577 	 33.94 	 222.70 	 51.37 	 651.21 	 1272.15 	 2667.76 	 ix 	 emperor
8 	 0.577 	 0.635 	 58.00 	 205.56 	 47.68 	 778.08 	 2023.78 	 3021.18 	 hv 	 had
9 	 0.635 	 0.728 	 92.50 	 196.31 	 58.89 	 751.70 	 1879.66 	 2955.21 	 ae 	 had
10 	 0.728 	 0.753 	 25.00 	 188.39 	 48.89 	 466.59 	 1774.32 	 2804.57 	 dx 	 had
11 	 0.753 	 0.785 	 32.50 	 189.57 	 51.92 	 625.67 	 1702.00 	 2759.04 	 ix 	 a
12 	 0.785 	 0.880 	 95.00 	 191.98 	 42.38 	 492.81 	 1558.76 	 2522.58 	 m 	 mean
13 	 0.880 	 0.975 	 95.00 	 203.83 	 45.36 	 817.94 	 2742.16 	 3309.95 	 iy 	 mean
14 	 0.975 	 1.045 	 70.06 	 208.15 	 42.32 	 776.86 	 2729.41 	 3184.43 	 n 	 mean
15 	 1.045 	 1.083 	 37.44 	 nan 	 22.54 	 590.77 	 1840.08 	 2627.61 	 tcl 	 temper
16 	 1.083 	 1.149 	 66.25 	 nan 	 35.59 	 863.97 	 2085.66 	 3048.04 	 t 	 temper
17 	 1.149 	 1.235 	 86.25 	 186.68 	 50.61 	 731.56 	 1777.54 	 2924.53 	 eh 	 temper
18 	 1.235 	 1.274 	 39.12 	 174.39 	 37.99 	 330.46 	 1662.65 	 2384.85 	 m 	 temper
19 	 1.274 	 1.313 	 39.00 	 nan 	 21.16 	 1398.41 	 2749.36 	 3894.47 	 pcl 	 temper
20 	 1.313 	 1.343 	 29.37 	 nan 	 34.37 	 789.79 	 1594.63 	 3228.07 	 p 	 temper
21 	 1.343 	 1.417 	 75.00 	 85.47 	 44.62 	 555.23 	 1360.27 	 2090.98 	 axr 	 temper

6. NLTK 이용하여 POS tag 가져오기

In [7]:
import nltk
In [8]:
sentence = "the emperor had a mean temper"
text = nltk.word_tokenize(sentence)
pos = nltk.pos_tag(text)
print(pos)
for i, t in enumerate(pos):
    print(i+1, t)
[('the', 'DT'), ('emperor', 'NN'), ('had', 'VBD'), ('a', 'DT'), ('mean', 'JJ'), ('temper', 'NN')]
1 ('the', 'DT')
2 ('emperor', 'NN')
3 ('had', 'VBD')
4 ('a', 'DT')
5 ('mean', 'JJ')
6 ('temper', 'NN')
In [9]:
import collections
ord_dict = collections.OrderedDict()

for i, t in enumerate(pos):
    ord_dict[t[0]] = (i, t[1])

for k, v in ord_dict.items():
    print(k, v[0], v[1])
    
the 0 DT
emperor 1 NN
had 2 VBD
a 3 DT
mean 4 JJ
temper 5 NN
In [10]:
import glob, re, tgt
for tg_file in glob.glob('data/*.TextGrid'):
    
    # 텍스트그리드 파일을 읽어들임
    print("processing {}...".format(tg_file))
    
    # 디렉토리와 파일명이 /로 결합되어 있는 데, 이를 분리하고, 파일명 앞까지 
    # 분리된 디렉토리들을 결합하여 디렉토리명으로 지칭함. 

    dir_name = tg_file.split('/')[:-1]
    dir_name = '/'.join(dir_name)
    print("directory name... {}".format(dir_name))
    
    # 마지막 [-1]로 된 것은 파일명이며, 텍스트그리드를 읽어들였으니 tg_name으로 명명함
    tg_name = tg_file.split('/')[-1]
    print('textgrid name... {}'.format(tg_name))
    
    # wave 파일과 TextGrid 파일은 이름을 공유하고 있으므로 extension만 wav로 바꾸고
    # wav_name으로 저장함. 
    wav_name = re.sub("TextGrid", "wav", tg_name)
    print("wave file name...{}".format(wav_name))
    
    # tgt를 이용해 textgrid 파일을 읽어들임.
    tg = tgt.read_textgrid(dir_name + '/' + tg_name)

    # tg의 파일명 알아보기
    tgname = tg.filename
    print("textgrid의 파일명...{}".format(tgname))
    
    #textgrid tier name 알아보기
    tg_tier_names = tg.get_tier_names()
    print("textgrid tier names...{}".format(tg_tier_names))
    
    # 찾아낸 tier명을 사용사여 tier들을 지정하기
    phn_tier = tg.get_tier_by_name('phn')
    wrd_tier = tg.get_tier_by_name('wrd')

    # 
    overlap_tier = tgt.util.get_overlapping_intervals(phn_tier, wrd_tier)
    #print('overlap_tier...{}'.format(overlap_tier))
    print('idx','\t','wordpos','\t','start','\t','end','\t',
          'dur','\t','phone','\t','pos','\t','word')
    
    for i, t in enumerate(overlap_tier):
        #print(i, t)
        texts = t.text.split('+')
        duration = (t.end_time-t.start_time)*1000 # miliseconds
        #print(ord_dict[texts[1]])
        print(format(i),'\t',
              format(ord_dict[texts[1]][0]+1),'\t',
              format(t.start_time, ".3f"),'\t',
              format(t.end_time, ".3f"),'\t',
              format(duration, ".2f"),'\t',
              format(texts[0]),'\t',
              format(ord_dict[texts[1]][1]),'\t',
              format(texts[1]))
 
processing data/SX127.TextGrid...
directory name... data
textgrid name... SX127.TextGrid
wave file name...SX127.wav
textgrid의 파일명...data/SX127.TextGrid
textgrid tier names...['phn', 'ipa', 'wrd']
idx 	 wordpos 	 start 	 end 	 dur 	 phone 	 pos 	 word
0 	 1 	 0.139 	 0.177 	 37.69 	 dh 	 DT 	 the
1 	 1 	 0.177 	 0.235 	 57.69 	 iy 	 DT 	 the
2 	 2 	 0.315 	 0.376 	 61.12 	 eh 	 NN 	 emperor
3 	 2 	 0.376 	 0.427 	 50.13 	 m 	 NN 	 emperor
4 	 2 	 0.427 	 0.442 	 15.31 	 pcl 	 NN 	 emperor
5 	 2 	 0.442 	 0.497 	 55.00 	 p 	 NN 	 emperor
6 	 2 	 0.497 	 0.543 	 46.19 	 r 	 NN 	 emperor
7 	 2 	 0.543 	 0.577 	 33.94 	 ix 	 NN 	 emperor
8 	 3 	 0.577 	 0.635 	 58.00 	 hv 	 VBD 	 had
9 	 3 	 0.635 	 0.728 	 92.50 	 ae 	 VBD 	 had
10 	 3 	 0.728 	 0.753 	 25.00 	 dx 	 VBD 	 had
11 	 4 	 0.753 	 0.785 	 32.50 	 ix 	 DT 	 a
12 	 5 	 0.785 	 0.880 	 95.00 	 m 	 JJ 	 mean
13 	 5 	 0.880 	 0.975 	 95.00 	 iy 	 JJ 	 mean
14 	 5 	 0.975 	 1.045 	 70.06 	 n 	 JJ 	 mean
15 	 6 	 1.045 	 1.083 	 37.44 	 tcl 	 NN 	 temper
16 	 6 	 1.083 	 1.149 	 66.25 	 t 	 NN 	 temper
17 	 6 	 1.149 	 1.235 	 86.25 	 eh 	 NN 	 temper
18 	 6 	 1.235 	 1.274 	 39.12 	 m 	 NN 	 temper
19 	 6 	 1.274 	 1.313 	 39.00 	 pcl 	 NN 	 temper
20 	 6 	 1.313 	 1.343 	 29.37 	 p 	 NN 	 temper
21 	 6 	 1.343 	 1.417 	 75.00 	 axr 	 NN 	 temper

References

Jadoul, Y., Thompson, B., & de Boer, B. (2018). Introducing Parselmouth: A Python interface to Praat. Journal of Phonetics, 71, 1-15. https://doi.org/10.1016/j.wocn.2018.07.001

Buschmeier, H. and Włodarczak, M. (2013). TextGridTools: A TextGrid Processing and Analysis Toolkit for Python. In P. Wagner (ed.) Tagungsband der 24. Konferenz zur Elektronischen Sprachsignalverarbeitung (ESSV 2013). Dresden: TUDpress, pp. 152-157. https://pub.uni-bielefeld.de/download/2561620/2563287

In [ ]:
 
In [ ]:
 
In [ ]: