Problem syntezowania mowy fascynował ludzi od dawna (pierwsze syntezatory budowano już w XVIII wieku), jednak w przeciwieństwie do innych zagadnień, o których można by powiedzieć to samo (transmutacja metali w złoto, eliksir młodości, pytanie czy P=NP i tak dalej), znalazł on dziś całkiem satysfakcjonujące rozwiązania. A to dzięki rozwojowi informatyki - łatwiej napisać program niż zbudować maszynę, która w zależności od danych wejściowych miałaby wydawać najrozmaitsze dźwięki. Pisaniem takich właśnie programów się zajmiemy.
W cyklu artykułów, którego pierwszą część masz właśnie, Czytelniku, przed oczyma, postaram się opowiedzieć o tym, jak stworzyć prosty syntezator mowy. Nie należy oczekiwać, że zbliżymy się poziomem zaawansowania do znanej Ivony (http://ivona.com), mam jednak nadzieję, że poznamy całą esencję konstrukcji syntezatorów i zobaczymy na czym cała ta zabawa polega. Nasz cel będzie taki: stworzyć syntezator potrafiący wypowiadać dowolne frazy w języku polskim tak, by dało się je zrozumieć.
Pierwszą rzeczą, o jaką musimy zadbać przy syntezie mowy, jest umiejętność przekształcania ciągów liter na ciągi głosek. Tym właśnie zajmiemy się w tej części cyklu. Zadanie na pierwszy rzut oka mogłoby wydać się banalne, szczególnie jeśli interesuje nas język polski, gdzie przecież praktycznie zawsze czytamy słowa "tak jak się pisze". Spójrzmy jednak:
Słowa wymawiamy zupełnie inaczej, niż je zapisujemy! Dlaczego tak się dzieje? Czy ma to jakiś sens ("sęs") i czy potrafimy stworzyć algorytm, który będzie mógł określić wymowę dowolnego słowa? Otóż okazuje się, że tak: ma to sens i da się stworzyć odpowiedni algorytm.
Jednak by się do tego zabrać, należałoby najpierw przypomnieć sobie parę podstawowych wiadomości na temat głosek i ich klasyfikacji. Za chwilę pojawi się sporo zdań, w których przewijać będą się określenia takie jak głoska dźwięczna, szczelinowa, zwarta etc. Odwagi jednak! Nawet jeśli owe określenia przywołują koszmary z dawno zapomnianych lekcji, zapewniam: nie ma się czego bać, bo w istocie są to rzeczy banalnie proste.
Przede wszystkim powiedzmy, że każdą głoskę określa szereg cech: mamy głoski dźwięczne i bezdźwięczne; ustne i nosowe; otwarte, półotwarte, szczelinowe, zwarto-szczelinowe, zwarto-wybuchowe; miękkie i twarde. Wiem, brzmi to jak czarna magia, ale spróbujmy posłużyć się podstawową bronią informatyków - logiką. Omówimy teraz szczegółowo podane wyżej pojęcia, ograniczając się atoli do tych, które są istotne dla nas jako twórców syntezatorów mowy.
O dźwięczności lub bezdźwięczności głosek decydują wiązadła głosowe w krtani - jeśli przy artykulacji głoski są zsunięte, to powietrze wydostające się z płuc wprawia je w drgania i powstająca głoska jest dźwięczna, w przeciwnym wypadku - powietrze przepływa swobodnie i głoska jest bezdźwięczna. W jaki sposób określić, czy dana głoska jest dźwięczna czy bezdźwięczna? Bardzo łatwo, wystarczy pamiętać o kilku rzeczach:
Głoska jest nosowa, jeśli przy jej wymawianiu powietrze wychodzi również przez nos. Spółgłoski nosowe to "m", "n" i "ń", natomiast samogłoski nosowe to "ą" i "ę". Samogłoska "ą" to tak naprawdę unosowione "o" (zauważmy, że w pierwszej fazie jej wymawiania słychać /o/) - unosowione "a" w języku polskim również występuje (np. w wyrazie "tramwaj" -> /t r an w a j/), jest jednak rzadkie i nie będziemy sobie nim zaprzątać głowy
Spółgłoski dzielimy ze względu na stopień zbliżenia narządów mowy na trzy ważne grupy:
Spróbujmy teraz zebrać powyższe informacje i stworzyć tabelę właściwości głosek. Umieścimy w niej jedynie spółgłoski, gdyż klasyfikacja samogłosek jest mniej ważna (nam wystarczy jedynie rozróżniać samogłoski nosowe od nienosowych). Głoski, z których nie będziemy korzystać zaznaczymy kursywą - warto jednak wiedzieć, że takie istnieją. Apostrof po spółgłosce (np. /m'/) oznacza jej zmiękczenie, np. w wyrazie /m' j a s t o/ - my nie będziemy się tym przejmować i powiemy po prostu /m j a s t o/. Zauważmy, że głoski takie jak /m/ czy /r/ mogą być zarówno dźwięczne jak i bezdźwięczne - to również szczegół, który w dalszych rozważaniach pominiemy.
Głoski | wargowe | przedniojęzykowe | środkowojęzykowe | tylnojęzykowe | |||||||||||||||||||||
dwuwargowe | Wargowo-zębowe | zębowe | dziąsłowe | Prepalatalne | Postpalatalne | ||||||||||||||||||||
twarde | zmięk | twarde | zmięk | twarde | zmięk | twarde | zmięk | twarde | zmięk | ||||||||||||||||
dźw | bez | dźw | bez | dźw | bez | dźw | bez | dźw | bez | dźw | bez | dźw | bez | dźw | bez | dźw | bez | dźw | bez | dźw | bez | dźw | bez | ||
Spółgłoski właściwe | zwarte | b | p | b' | p' | d | t | d' | t' | gi | ki | g | k | ||||||||||||
Zwarto-szczelinowe | dz | c | dz' | c' | drz | cz | drz' | cz' | dzi | ci | |||||||||||||||
szczelinowe | v | f | v' | f' | z | s | z' | s' | rz | sz | rz' | sz' | zi | si | ch | ch | ch' | ch' | |||||||
spółgłoski półotwarte | nosowe | m | m | m' | m' | n | n | n' | n' | ni | ni | nga | nga | nga | nga | ||||||||||
drżące | r | r | r' | r' | |||||||||||||||||||||
boczne | l | l | l' | l' | |||||||||||||||||||||
półsamogłoskowe ustne | j | ll | ll | ll' |
No dobrze - teraz, gdy wiemy już czym tak naprawdę jest głoska, możemy wreszcie spróbować opisać ją w przyjaźniejszy dla nas sposób, tzn. pisząc sobie odpowiednią klasę w jakimś języku programowania! Ja wybrałem do tego celu Pythona (http://python.org), jako że jest to język odpowiednio wysokopoziomowy i wybitnie przyjazny programiście. Można by sarkać na jego niezbyt wielką wydajność, jednak byłoby to sarkanie pozbawiano sensu, gdyż akurat w syntezie mowy wydajność języka nie jest kluczem do sukcesu.
Chcemy zaprojektować klasę opisującą wszystkie interesujące nas własności głosek. Do tego celu nasz program będzie musiał posiadać jakąś wiedzę - dajmy mu ją więc! Proponuję utworzyć plik Gloska.txt i wpisać do niego:
#stopien_zblizenia zwarto-wybuchowa:b,p,d,t,g,k zwarto-szczelinowa:dz,c,dzi,ci,drz,cz,ci szczelinowa:v,w,f,z,s,rz,sz,zi,si,ch nosowa:m,n,ni drżąca:r boczna:l półsamogłoskowa:ll,j otwarta:a,an,o,on,e,en,y,i,u #dzwieczne b,m,v,w,d,dz,z,n,dzi,zi,ni,drz,rz,r,g,a,an,o,on,e,en,y,i,u
Teraz trzeba będzie wczytać te dane do programu. Tworzymy zbiór głosek dźwięcznych, oraz słownik, w którym kluczami będą nazwy głosek, a wartościami - stopień zbliżenia narządów mowy przy ich artykulacji. Pokażę tutaj jeszcze jak to zrobić, żebyśmy oswoili się z Pythonem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | dzSet = set() # zbiór dzSet głosek dźwięcznych szDic = {} # słownik szDic ze stopniem zbliżenia narządów mowy dla każdej głoski for line in open('Gloska.txt'): # czytamy plik linia po linii if line.startswith('#stopien_zblizenia'): # sprawdzamy w której części pliku jesteśmy part = 0 continue elif line.startswith('#dzwieczne'): part = 1 continue if part == 0: val, keys = line.strip().decode('utf-8').split(':') for k in keys.split(','): szDic[k] = val elif part == 1: for gl in line.strip().decode('utf-8').split(','): dzSet.add(gl) |
Jest jeszcze jedna rzecz. Chcielibyśmy znać pary komplementarnych pod względem dźwięczności głosek, tzn. /d/ i /t/, /b/ i /p/ etc. Stwórzmy więc odpowiedni słownik (co prawda na razie nie będziemy z nich korzystać, ale - przydadzą się później):
1 2 3 4 5 6 7 8 9 | dz_bdz = [("b", "p"), ("dz", "c"), ("dzi", "ci"), ("drz", "cz"), ("d", "t"), ("w", "f"),("g", "k"),("z", "s"),("zi", "si"),("rz", "sz")] dz_bdz = dict(dz_bdz + map(lambda (x, y): (y,x), dz_bdz)) """ Przykład użycia: >>> dz_bdz['t'] 'd' """ |
Mając te przygotowania za sobą, jesteśmy gotowi do stworzenia naszej klasy, nazwijmy ją Gloska. Chcemy móc napisać a = Gloska('a') i w zmiennej a mieć obiekt w pełni opisujący głoskę /a/. Zacznijmy więc:
1 2 3 4 5 6 7 8 9 | class Gloska: def __init__(self, name): self.name = name self.dzwieczna = name in dzSet self.stopien_zblizenia = szDic[name] #typ określa, czy głoska jest samogłoską czy spółgłoską self.typ = u'samogłoska' if self.stopien_zblizenia == u'otwarta' else u'spółgłoska' self.nosowa = name[-1] == u'n' or self.stopien_zblizenia == u'nosowa' |
Et Voila! Właśnie powstała klasa, którą będziemy odtąd posługiwać się bardzo często. Dodajmy jej metody pozwalające na ładne wyświetlanie:
1 2 3 4 5 | def __repr__(self): return u'<"%s"(%s)>' % (self.name, u'dz' if self.dzwieczna else u'bdz') def __str__(self): return u'<\%s>' \% self.name |
Odpalmy teraz interpreter Pythona i spróbujmy pobawić się naszą klasą:
1 2 3 4 5 | >>> en = Gloska('en') # en jest teraz obiektem przechowującym głoskę /en/ >>> print en # wypiszmy go sobie ładnie <en> >>> en.stopien_zblizenia # zdobądźmy jakieś informacje o naszej głosce u'otwarta' |
Jak widać, wszystko działa zgodnie z naszymi oczekiwaniami, możemy więc przejść dalej
Mając niezbędne przygotowania za sobą, zastanówmy się - jak zamieniać słowa stworzone z liter w ciągi głosek? Jak zostało wspomniane wyżej, pierwszym, najoczywistszym pomysłem jest znalezienie odpowiedniości litera-głoska, litery-głoska bądź litera-głoski (bo przecież nie zawsze jedna litera odpowiada jednej głosce). Rozwijając ten pomysł, napisalibyśmy system regułowy, z instrukcjami typu ciąg liter -> ciąg głosek:
a -> a ą -> on b -> b c -> c ci -> ci,i ć -> ci d -> d e -> e ę -> en (...)
...i tak dalej. Łatwo jednak przekonać się, że takie proste przypisania nie wystarczą, gdyż język zawiera wiele mechanizmów ułatwiających wymowę (a nam utrudniających zadanie), inaczej mówiąc: te same litery możemy w różnych sytuacjach czytać inaczej - zauważyliśmy to już wcześniej. Usystematyzujmy naszą wiedzę o wszystkich takich zjawiskach.
Spójrzmy na przykłady:
bęben -> /b e m b e n/
dętka -> /d e n t k a/
dąb -> /d o m b/
wątły -> /w o n t ll y/
Wbrew pozorom tak właśnie te słowa powiemy (a gdybyś, Czytelniku, miał wątpliwości, spróbuj je sobie parę razy na głos dość szybko powtórzyć) i jest to podyktowane odwiecznym lenistwem przyrody. Przypomnijmy sobie to, czego przed chwilą nauczyliśmy się o głoskach i zobaczmy: /m/ i /b/ (w słowie /b e m b e n/) - obie są dwuwargowe, /n/ i /t/ (/d e n t k a/) - obie są przedniojęzykowo-zębowe! Niesamowity zbieg okoliczności, prawda? Tak, bo to wcale nie przypadek. Łatwiej nam w wymowie przejść między dwiema głoskami artykułowanymi w tym samym miejscu, niż między dwiema całkowicie różnymi - dlatego właśnie zamiast powiedzieć soczyste /en/ i zaraz potem z trudem przejść do /t/, powiemy po prostu /e/, następnie - by nie utracić nosowości - /n/ i spokojnie - /t/. Ale jest jeszcze jeden powód tego, że wymowa samogłosek nosowych staje się tak "niechlujna" (fachowo nazwiemy ją po prostu "asynchroniczną") akurat przed głoskami zwartymi, a przed żadnymi innymi - nie. Zauważmy, że zarówno wymowa /m/ i /n/ stworzy zwarcie; zwarcie to posłuży nam do wypowiedzenia kolejnej głoski - ot, i cała tajemnica.
Teraz pytanie - jak ująć to w naszym programie? Można albo napisać sobie funkcję, która, korzystając z wiedzy o głoskach, zmodyfikuje wymowę każdego słowa, lub po prostu dodać parę reguł (w końcu tak dużo tych spółgłosek zwarty-wybuchowych i zwarto-szczelinowych nie ma) wyglądających tak je te:
ęb -> e,m,b ęp -> e,m,p ęt -> e,n,t ęd -> e,n,d ęc -> e,n,c (...)
Ponownie zacznijmy od przykładów:
wąs -> /w on s/
kęs -> /k en s/
sens -> /s en s/
konfrontacja -> /k on f r o n t a c j a/
tramwaj -> /t r an w a j/
O ile wymowa słów "wąs" i "kęs" jest zgodna z naszymi intuicjami, to kolejne trzy przypadki mogą budzić wątpliwości - co tam się dzieje? Zauważmy, że mamy kolejno: samogłoskę, spółgłoskę nosową (/n/, /m/), spółgłoskę szczelinową(/s/, /f/, /w/). To znów nie przypadek, kolejny raz decyduje ekonomia: zwarcie powstające przy artykulacji /n/ czy /m/ znacząco utrudnia przejście do artykułowania głoski szczelinowej, zatem - myśli sobie część naszego mózgu odpowiedzialna za wymowę - nie twórzmy zwarcia! Wrzucamy spółgłoskę nosową do samogłoski, która sama staje się przez to unosowiona.
Ponownie mamy dwie możliwości: możemy tę zasadę zaszyć w kodzie, lub wrzucić na sztywno do pliku z regułami.
Tutaj sytuacja jest jasna i tyczy się form czasowników zakończonych na "-ęła", "-ęły" lub "-ął", które artykułujemy /e ll a/, /e ll y/ i /o ll/. Również nosowość samogłoski "-ę" na końcu wyrazów znika i wymawiamy ją po prostu /e/.
Jako że mówimy tutaj o sufiksach, warto do naszego pliku wprowadzić notację zapożyczoną z wyrażeń regularnych a pozwalającą na oznaczanie początku (^) i końca ($) wyrazów. Dzięki temu będziemy mogli zapisać:
Spójrzmy:
romantyzmie -> /r o m a n t y zi m j e/
windzie -> /w i ni dzi e/
rozdziawił -> /r o zi dzi a w i ll/
Żeby zrozumieć dlaczego tak się dzieje, należy wiedzieć, że za miękkość spółgłosek odpowiada środkowa część języka, która zbliża się do podniebienia twardego. Obie spółgłoski wymawiamy zmiękczone z lewnistwa - język ma do przebycia krótszą drogę.
Znów jawią się dwie możliwości rozwiązania tej kwesti. Zdecydowanie łatwiejsze wydaje się znowu dodanie sztywnych reguł zamiany, ale oczywiście można próbować to zaprogramować - musielibyśmy w takim przypadku dodać do naszych głosek informacje o miękkości i funkcje służące do zmiękczania.
Upodobnienia dotyczą dźwięczności spółgłosek i znów łatwo wyjaśnić skąd się biorą - łatwiej pozostawić wiązadła głosowe w jednej pozycji niż cały czas zsuswać je lub rozsuwać. Upodobnienia dzielimy na trzy rodzaje:
Tutaj możliwości jest już zbyt wiele, by objąć je regułami, nie będziemy więc tego robić, zwłaszcza że - mając klasę Gloska z odpowiednimi polami oraz metodami - łatwo napiszemy dwie funkcje:
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 | def backward(phones): ''' przechodzi przez głoski i uskutecznia upodobnienie wsteczne ''' for i in range(len(phones)-1, 0, -1): if not (phones[i].isVowel() or phones[i-1].isVowel() or phones[i].stopien_zblizenia in [u'półsamogłoskowa',u'drżąca', u'nosowa', u'boczna']) and ( phones[i].dzwieczna != phones[i-1].dzwieczna): phones[i-1].zmien_dzwiecznosc() def forward(phones): ''' przechodzi przez głoski i uskutecznia upodobnienie postępowe ''' for i in range(1, len(phones)): if (phones[i].name in [u'w', u'rz', u'r'] and phones[i].dzwieczna and not phones[i-1].dzwieczna and phones[i-1].stopien_zblizenia not in [u'nosowa', u'boczna', u'półsamogłoskowa'] and not phones[i-1].isVowel()): phones[i].ubezdzwiecznij() return phones |
Zauważmy, że korzystamy tutaj z funkcji klasy Gloska: zmien_dzwiecznosc() oraz ubezdzwiecznij(). Co prawda jeszcze ich nie napisaliśmy, jest to jednak na tyle proste zadanie, że pozostawimy je jako ćwiczenie dla Czytelnika (w tym właśnie miejscu należy skorzystać z naszego słownika przechowującego pary komplementarnych pod względem dźwięczności głosek - przypomnijmy, że na przykład udźwięcznione /t/ to /d/).
Musimy pamiętać jeszcze o paru sprawach:
Korzystając ze zdobytej dotychczas wiedzy, jesteśmy w stanie zaprojektować algorytm przekształcający ciąg liter na ciąg głosek. Wyglądałby on tak:
1 2 3 4 | wczytaj słowo "w" zastosuj na słowie "w" reguły z pliku "rules.txt" i otrzymaj wstępny ciąg głosek "g" zastosuj na "g" dodatkowe funkcje (np. realizacja upodobnień i zamiana niesylabotwórczych "i" na "j") zwróć "g" |
Napisanie funkcji aplikującej reguły nie jest zadaniem przesadnie trudnym, zatem znów pozostawię je Czytelnikowi do samodzielnego rozważenia, podpowiadając jedynie, że warto próbować stosować reguły w kolejności od najdłuższej do najkrótszej.
W tak rozpisanym algorytmie kluczowym jawi się stworzenie dobrych reguł. Proponuję następującą zabawę: poniżej mamy program z zaimplementowanym algorytmem opisanym wyżej i z dodanym zestawem reguł, który wszakże nie jest kompletny. Postaraj się dodać brakujące reguły tak, aby wyniki konwersji słów do ciągów głosek zgadzały się z naszymi oczekiwaniami, czyli z wymową podaną w nawiasach kwadratowych (pamiętaj o notacji "^" oraz "$" - może się przydać).
Każdą regułę oddziel znakiem |, jeśli chcesz przejść do następnej linii, zakończ poprzednią przy pomocy "\".
Przykład: ^zin -> z,i,n|\ ęł -> e,ll | |
|
Podsumujmy tę część. Przyjrzelliśmy się zasadom czytania tekstów w języku polskim i powiedzieliśmy sobie jak napisać program konwertujący tekst pisany na ciągi głosek do wypowiedzenia. Postawiliśmy pierwszy solidny krok na drodze do napisania syntezatora mowy. W następnej części Akcent i zabawa dźwiękiem [2] pogawędzimy trochę o tym, czym tak naprawdę jest dźwięk oraz przyjrzymy się problemom związanym z akcentowaniem. Do poczytania!
Odnośniki:
[1] http://informatyka.wroc.pl/node/377
[2] http://informatyka.wroc.pl/node/445