Festival note

本網頁以打造無障礙閱讀為目標,可以用任何瀏覽器來觀看本網頁


緣起

我們可以發現雖然在國外有像Festival等的open source語音合成軟體可供研究,但並未針對中文作處理,而國內的研究雖然先進但尚未能找到開放原始碼的版本,因此希望能善用Festival現有的成果,不要重新發明 輪子,期許創造一個雖然距離國內許多先進研究成果差一段距離,但卻是一個屬於中國人專屬的語音合成自由軟體,同時藉由此計劃拋磚引玉的效果快速的集聚OSS的社群力量,將 Festival修改後使其能夠做國語語音合成的基石。

簡介(Introduction)

Festival這套語音合成系統是由蘇格蘭愛丁堡大學(University of Edinburgh)因為愛丁堡藝術節而命名
  1. 最新版本 : 1.95-beta (2004/07/14)
  2. 主要作者 : Alan W Black
  3. 開發語言 :
  4. 發音語言 : 英語(英式,美式),西班牙語,威爾斯語
  5. Festival這套軟體所使用的合成語音有一個子計畫叫做FestVox

安裝

以rpm安裝(若只要使用festival來發聲)

下載點

安裝步驟

#rpm -Uvh fest*

以tarball安裝(若同時要自製合成語音檔)

下載點

檔案清單

主要系統套件
speech_tools-1.2.3-release.tar.gz 語音工具的函式庫與源碼檔
festival_1.4.3-release.tar.gz festival的源碼檔
festvox-2.0-release.tar.gz festvox的源碼檔
辭典套件
festlex_POSLEX.tar.gz 語音零件的辭典模式
festlex_CMU.tar.gz 美式英語辭典(需結合美式聲音檔)
  festlex_OALD.tar.gz 英式英語辭典(需結合英式聲音檔)
主要聲音套件
festvox_kallpc16k.tar.gz 美式男聲 `kal' 16kHz版本
  festvox_kallpc8k.tar.gz 美式男聲 `kal' 8kHz版本
  festvox_kedlpc16k.tar.gz 美式男聲 `ked' 16kHz版本
  festvox_kedlpc8k.tar.gz 美式男聲 `ked' 8kHz版本
  festvox_rablpc16k.tar.gz 英式男聲 `rab' 16kHz版本
  festvox_rablpc8k.tar.gz 英式男聲 `rab' 8kHz版本
其他聲音套件
  festvox_us1.tar.gz 其他不完整的美式聲音檔1
  festvox_us2.tar.gz 其他不完整的美式聲音檔2
  festvox_us3.tar.gz 其他不完整的美式聲音檔3
  festvox_don.tar.gz 其他不完整的美式聲音檔
  festvox_en1.tar.gz 其他不完整的美式聲音檔
  festvox_ellpc11k.tar.gz 西班牙聲音檔
標註*者為最小安裝所需套件

安裝步驟

假設使用者家目錄為peter, 請將上列下載的所有檔案複製到使用者家目錄中的 projects 子目錄中,以下步驟也可整個寫成一支shell script方便執行。

  1. 編寫script解壓縮所有檔案,會得到3個目錄 speech_tools, festival, festvox
    #!/bin/sh
    for i in *.gz
    do
      tar -zxvf $i
    done
  2. 到speech_tools目錄下,執行 ./configure, gmake, gmake test
    cd speech_tools
    ./configure
    make
    make test
    make install
  3. 到festival目錄下,執行 ./configure, gmake, gmake test
    cd ../festival
    ./configure
    make
    make test
    make install
  4. 增加4個環境變數(~/.bashrc)
    export PATH=$PATH:/home/peter/project/festival/bin
    export PATH=$PATH:/home/peter/project/speech_tools/bin
    export ESTDIR=/home/peter/project/speech_tools
    export FESTVOXDIR=/home/peter/project/festvox
  5. 到festvox目錄下,執行 make
    cd festvox
    ./configure
    make

使用

shell下

#festival //呼叫交談模式
#echo "hello world" | festival --tts //echo 字串給 festival發音(2個dash喔)
#festival --tts news.txt //命令模式下直接唸出指定的檔名內容(2個dash喔)
#festival -b filename.scm //命令模式下執行指定的批次檔
#/usr/share/festival/examples/saytime
//範例:說出現在時間(以rpm安裝時的目錄)

festival內

festival 因為連結了readline函式庫,因此按鍵方式與shell下與emacs相同

festival>(SayText "hello") //唸出指定的文字
festival>(SayPhone "hh eh l ow") //唸出指定的音素(音素的種類需視所採用的音素集而定,目前系統有 US phonesetUK phoneset ) 二種
festival>(voice_kal_diphone) 或 (voice_ked_diphone)
//更換語音資料為kal 或 ked
1.95版的女聲:  (voice_cmu_us_slt_arctic_hts)
festival>(PhoneSet.list)
//列出目前所使用的音素集 (radio)
festival>(voice_rab_diphone)
//更換語音資料為rab
festival>(PhoneSet.list)
//列出目前所使用的音素集 (radio mrpa)
festival>(PhoneSet.description '(silences phones)) //列出所有可用的音素
festival>(tts "filename") //可唸出所指定的檔名內容
festival>(Parameter.set 'Duration_Stretch 2.0) //將發音的時間拉長(唸慢一點)
festival>help //Help
festival>(quit) //離開交談模式,或按Ctrl+D
festival>(lex.lookup 'hello) //查出hello這個字的音素為何
festival>(intro) //發音測試:2句話簡介Festival
festival>(festival_warranty) //顯示Festival的授權範圍
festival>libdir //library存放的目錄
festival>(pwd) //顯示目前目錄
festival>(pow 5 3) //5的3次方,顯示125
festival>(load 'test.scm) //載入scheme的script檔

設定檔

在festival內鍵過的命令:~.festival_history

Festival 的語音架構 (Speech Architecture)

本文主要在探討語音合成的資料結構,在此使用了語音合成的自由軟體巨擘Festival(http://www.festvox.org/packed/festival/1.4.3/ ),來剖析,並以Lisp家族中的Scheme來作為研究的工具。

Festival這套語音合成系統是由蘇格蘭愛丁堡大學(University of Edinburgh)因為愛丁堡藝術節而命名 ,其核心以C++撰寫而成,但因為配合使用了SIOD函式庫,所以可以使用Scheme來控制與使用。 目前的穩定版本為1.43版,最新版本為2004/7/14的 1.95版。

卡內基美濃大學 配合 Festival 發展了一個子計畫:Festvox(http://festvox.org/),目的在提供更多樣化的語音與更好的語音品質與手冊文件。

產生Utterance物件

在安裝Festival完成之後,我們可以簡單的試一下要如何發聲,在提示符號下輸入 (SayText "This is an example")

festival> (SayText "This is an example")
#<Utterance 0x404054e8>

回應表示festival將使用者鍵入的文字轉換為一個名為Utterance的資料結構中,並儲存在記憶體0x404054e8位址上 ,因為後續要針對這個語音的資料結構進行拆解,因此了定義一個「sen」 的變數指向它,以便接下來可以對這個Utterance 結構進行剖析

festival> (set! sen (SayText "This is an example"))
#<Utterance 0x40554b98>

Utterance的結構

從語音學(phonetics)的層次上,至少可以將一句話(Utterance)從不同的觀點來解析,如詞組(phrase)、語詞(Word)、音節(Syllable)、音節(Segment)等,這些不同的觀點在此我們可以稱為Relations。 例如:從Word這個Relation來看,它是由 This<->is<->an<->example所串連而成,而這個「This」、「is」、「an」、「example」在此稱之為 item,而每一個item有一組屬性,用來描述本身的特徵,在此稱之為Features。

Utterance's relations Word Relation
Utterance relations的組成
Word relation的組成

如果不好理解的話,我們另外舉一個例子,有一個學生Peter (like utterance) ,他的社會關係(relations)可以有 朋友關係 (like word relation) 、學校關係(like syllable relation)、家庭關係(like segment relation),而以朋友關係為例,Peter認識Cathy、Cathy認識Tom、Tom認識Emma,這些人(like item)本身各有特色(like features),如性別,年齡,身高,血型等。

Peter's relations Friends Relation
Peter同學社會關係的組成
朋友關係的組成

而在Festival中對於語音(speech)或語言的(linguistic)的結構上也因而有三個主要的形式

  1. Items(項目) : Item 指的是一個單一的語言單元,像是 phone(音素), word(單字), syllable(音節) , syntactic node(語法上的節點) , intonation phrase(聲調片語) 等 。 每一個 Item 都有自己的 features(特徵值) ,用以描述本身的局部屬性,例如名稱,發音長短等。features 的內容可能是文數字或是函數。
  2. Relations(關係) :relation 將上述的 item 連結在一起,例如可能會有word(單字), 語法(syntax), syllable(音節) ,等的relation。 relations 通常以圖形的資料結構表示,有lists, trees ,而最常見的是雙向的鏈結串列,例如 :word 的 relation 就是一個雙向的鏈結串列,它依序連結了所有在 utterance 中的 word 。 relations 也可能是以樹的資料結構表示,例如:一個音節的 relation 包含了開始(onset),結束(coda),中心(nucleus),與韻律(rhyme)的結構 。 有一點很重要,那就是 items 可以有超過一個以上的 relation ,例如:一個語法的 relation是一個樹狀結構,它的樹葉是 words ,而這些words本身又包含在 word的 relation 。
  3. Utterances(語調) : utterances 由 items 組成,每個 item有許多的features,item 間可能有1到多個 relations。

鍊結串列型的relations

接下來我們來瞭解Word relation的組成,在此我們要利用上面定義的sen變數,先看看在Word relation有幾個 item

festival> (utt.relation.leafs sen 'Word)
(#<item 0x87a1680>
  #<item 0x87a2170>
  #<item 0x87a2418>
  #<item 0x87a26c8>)

在此我們看到了4個 item的指標, 我們也可以直接指定取得其Word relation的第一個item

festival> (utt.relation.first sen 'Word)
#<item 0x87a1680>

回應的是一個item資料結構,存放在記憶體中的0x87a1680,同時請注意utt.relation.leafsd的第1個item與utt.relation.first的item為相同的位址,同樣的為了取用方便,我們定義了一個變數firstword指向它

festival> (set! firstword (utt.relation.first sen 'Word))

既然我們有了一個名為firstword的item,我們當然要看看它有哪些features

festival> (item.features firstword)
((id "_5")
  (name "This")
  (pos_index 2)
  (pos_index_score 0)
  (pos "dt")
  (phr_pos "dt")
  (pbreak_index 1)
  (pbreak_index_score 0)
  (pbreak "NB"))

我們也可以指定只看單一的feature,例如:要看firstword這個item名為「name」的feature

festival> (item.feat firstword "name")
"This"

除了直接指定item的順序之外,我們還可以利用目前的item位置,往後取得下一個item或往前取得前一個item

festival> (set! secondword (item.next firstword))
#<item 0x87a2170>
festival> (item.feat secondword "name")
"is"
festival> (item.feat secondword "p.name")
"This"

我們也可以使用這樣的方法觀察這個utterance的其它relations,例如 Syllable, Segment等 ,而為什麼說這些relations是屬於鍊結串列型的呢?可以由utt.relation.leafs的第一個item與utt.relation.first的item其號碼相同得知

樹狀型的relations

接下來我們來瞭解 SylStructure relation的組成,在此我們要利用上面定義的sen變數,先看看在 SylStructure relation有幾個 item

festival> (utt.relation.leafs sen 'SylStructure)
(#<item 0x87a30b8>
  #<item 0x87c3060>
  #<item 0x87a57b8>
  #<item 0x87a59e8>
  #<item 0x87c2cb8>
  #<item 0x87c2d48>
  #<item 0x87a42d8>
  #<item 0x87a4758>
  #<item 0x87a48f8>
  #<item 0x87a4c78>
  #<item 0x87a4e18>
  #<item 0x87a4fb8>
  #<item 0x87a5158>
  #<item 0x87a54d8>
  #<item 0x87be800>)

同樣的我們看看 SylStructure relation的第1個item為何?

festival> (utt.relation.first sen 'SylStructure)
#<item 0x87a1680>

實驗歷程

語音結構初始化
(set! sen (Utterance Text "This is an example"))  定義 sen 變數指向Utterance物件
(utt.play sen)  會出現Feature Wave Not defined的錯誤
(utt.relation.first sen 'Word)  會出現Feature Word Not defined的錯誤
(utt.synth sen)  必須做完此波形合成的動作才不會有錯誤
(set! sen (SayText "This is an example"))  定義 sen 變數指向完整的Utterance物件結構 (可省略合成的步驟)
word relation的觀察
(set! firstword (utt.relation.first sen 'Word))  定義 firstword 變數指向Word relation 的第一個 item
(item.features firstword)  觀察 firstword這個 item的所有feature
(item.feat firstword "name")
(item.name firstword)
 觀察 firstword這個 item的其中叫「name」的feature
(item.feat firstword "n.name")  觀察 firstword這個 item的下一個 item的「name」feature
(set! secondword (item.next firstword))  定義 secondword 變數指向 firstword 的下一個 item
(item.feat secondword "name")  觀察 secondword這個 item的其中叫「name」的feature
(item.feat secondword "p.name")  觀察 secondword這個 item的上一個 item的「name」feature
(utt.features sen 'Word '(id name pos))  觀察sen 以Word relation時的 features(類似mapleaf自訂函數)
Syllable relation的觀察
(set! firstsyllable (utt.relation.first sen 'Syllable))  定義 firstsyllable 變數指向Syllable relation 的第一個 item
(item.features firstsyllable)  觀察firstsyllable這個 item的所有feature
(item.feat firstsyllable "name")
(item.name firstsyllable)
 觀察firstsyllable這個 item的其中叫「name」的feature
(item.feat firstsyllable"n.name")  觀察 firstsyllable這個 item的下一個 item的「name」feature
(set! secondsyllable (item.next firstsyllable))  定義 secondsyllable 變數指向 firstword 的下一個 item
(item.feat secondsyllable"id")  觀察 secondsyllable這個 item的其中叫「id」的feature
(item.feat secondsyllable"p.id")  觀察 secondsyllable這個 item的上一個 item的「id」feature
(utt.features sen 'Syllable '(id name stress))  觀察sen 以Syllable relation時的 features
Segment relation的觀察
(set! firstsegment (utt.relation.first sen 'Segment))  定義 firstsegment 變數指向Segment relation 的第一個 item
(item.features firstsegment)  觀察firstsegment這個 item的所有feature
(item.feat firstsegment"name")
(item.name firstsegment)
 觀察firssegment這個 item的其中叫「name」的feature
(item.feat firstsegment "n.name")  觀察 firstsegment這個 item的下一個 item的「name」feature
(set! secondsegment (item.next firstsegment))  定義 secondsegment 變數指向 firstsegment 的下一個 item
(item.feat secondsegment "id")  觀察 secondsegment這個 item的其中叫「id」的feature
(item.feat secondsegment "p.id")  觀察 secondsyllable這個 item的上一個 item的「id」feature
(utt.features sen 'Segment '(id name dur_factor end))  觀察sen 以Syllable relation時的 features
Phrase relation的觀察
(set! firstphrase (utt.relation.first sen 'Phrase))  定義 firstphrase 變數指向Phrase relation 的第一個 item
(item.features firstphrase)  觀察firstphrase這個 item的所有feature
(set! secondphrase (item.next firstphrase))  定義secondphrase 變數指向 firstphrase 的下一個 item (傳回nil)
(item.daughters firstphrase)  觀察 firstphrase 這個 item有哪幾個女兒 (有4個女兒)
(set! fpl (item.daughter1 firstphrase))  定義 fpl 變數指向 firstword這個 item的女兒
(item.features fpl)  觀察fpl的feature,得知是Word relation
(item.feat fp1 "R:SylStructure.daughter1.stress")  改由SylStructure的觀連,察看女兒的 stress feature
SylStructure relation的觀察
(set! firstword (utt.relation.first sen 'SylStructure))  定義 firstword 變數指向 SylStructure relation 的第一個 item
(item.features firstword)  觀察firstword這個 item的所有feature
(item.feat firstword "name")
(item.name firstword)
 觀察 firstword這個 item的其中叫「name」的feature
(item.feat firstword"n.name")  觀察 firstword這個 item的下一個 item的「name」feature
(item.daughters firstword)  觀察 firstword這個 item有哪幾個女兒 (只有1個女兒)
(set! firstsyl (item.daughter1 firstword))  定義 firstsyl 變數指向 firstword這個 item的女兒
(item.features firstsyl)  觀察 firstsyl的feature,得知是Syllable relation
(item.name (item.parent firstsyl))  觀察 firstsyl這個 item的父親,得知是Word relation:「This」
(item.daughters firstsyl)  觀察 firstsyl這個 item有幾個女兒 (有3個女兒)
(set! secondseg (item.daughter2 firstsyl))  定義 secondseg 變數指向 firstsyl這個 item的第2個女兒
(item.features secondseg)  觀察 secondseg 的feature,得知是Segment relation
(utt.features sen 'SylStructure '(id name)  觀察sen 在 SylStructure relation時的 features

觀察整理

關係 資料結構 特徵
Word list
  1. id
  2. name
  3. pos_index
  4. pos_index_score
  5. pos
  6. phr_pos
  7. pbreak_index
  8. pbreak_index_score
  9. pbreak
Syllable list
  1. id
  2. name
  3. stress
Segment list
  1. id
  2. name
  3. dur_factor
  4. end
  5. source_end
Phrase

trees

  roots are Phrase
  leaves are Words

  1. id
  2. name
  1. id
  2. name
  3. pos_index
  4. pos_index_score
  5. pos
  6. phr_pos
  7. pbreak_index
  8. pbreak_index_score
  9. pbreak
SylStructure

trees

  roots are Words
  middles are Syllables
  leaves are Segments

  1. id
  2. name
  3. pos_index
  4. pos_index_score
  5. pos
  6. phr_pos
  7. pbreak_index
  8. pbreak_index_score
  9. pbreak
  1. id
  2. name
  3. stress
  1. id
  2. name
  3. dur_factor
  4. end
  5. source_end

自訂函數

(define (valot utt rel)
   to_end (utt.relation.first utt rel)
)

(define (to_end node)
   (if node
      (cons (sub_tree node) (to_end (item.next node)))
      nil
   )
)

(define (sub_tree node)
   (if (item.daughters node)
      (mapcar sub_tree (item.daughters node))
      node
   )
)

(define (mapleaf fun 1st)
   (cond
    ((null? 1st) nil)
    ((atom 1st) (fun 1st))
    (t (cons (mapleaf fun (car 1st)) (mapleaf fun (cdr 1st))))
   )
)

word relation的觀察
(valot sen 'Word)  以自訂函數觀察其4個 item是以鍊結串列結構
(mapleaf item.features (valot sen 'Word))  以自訂函數觀察4個 item各有何種 features (有9個)
Syllable relation的觀察
(valot sen 'Syllable)  以自訂函數觀察其6個 item也是以鍊結串列結構
(mapleaf item.features (valot sen 'Syllable))  以自訂函數觀察6個 item各有何種 features (有3個: id,name,stress)
Segment relation的觀察
(valot sen 'Segment)  以自訂函數觀察其17個 item也是以鍊結串列結構
(mapleaf item.features (valot sen 'Segment))  以自訂函數觀察17個 item各有何種 features (有5個: id,name,dur_factor, end, source_end)
SylStructure relation的觀察
(valot sen 'SylStructure)  以自訂函數觀察
(mapleaf item.features (valot sen 'SylStructure))  以自訂函數觀察

使用festival發出中文語音的實驗

首先想到的是利用羅馬拼音以 SayText 來試試看,發出 "我的名字叫朱孝國"

festival>(SayText "wo di ming zi giao ju hsiao koa" )

感覺還不錯,但是要這樣試過所有的音也太累了,況且也無法解決tone的問題,因此改由從festival的架構上來觀察,首先拿"hello"這個字來看看在word的層次的作法

festival>(set ! sen (Utterance Words (hello)))
festival>(utt.synth sen)
festival>(utt.play sen)
#<Utterance 0x408cb4d8>

以 grep 搜尋 "SayText" 發現在 lib\synthesis.scm中有定義 ,另外定義了"SynthText"與"SayPhones"

可說明,一個Text 的Utterance必須先經由utt.synth然後才可以utt.play播出聲音來

(define (SayText text) (utt.play (utt.synth (eval (list 'Utterance 'Text text)))))

(define (SynthText text) (utt.synth (eval (list 'Utterance 'Text text))))

(define (SayPhones phones) (utt.play (utt.synth (eval (list 'Utterance 'Phones phones)))))

 festival中的 scheme script 大都放在 lib 目錄下的 synthesis.scm 與 festival.scm
  • utt.synth
  • utt.play
    • wave.play
      • utt.wave

很明顯的要從utt.synth與utt.play這兩個函數開始追蹤,首先追蹤utt.play

再來追蹤utt.wave---------------------------------------->

(define (utt.play utt) (wave.play (utt.wave utt)) utt)

(define (utt.wave utt) (item.feat (utt.relation.first utt "Wave") "wave"))

以上2個函數沒什麼可下手的地方,再來追蹤utt.synth-------------------------------------------------->

在倒數第4行插入 (print body) ,存檔後重新載入festival ,重新設定變數,重新作(utt.synth sen)
會出現如下顯示,看起來要作10個動作

(CLOSURE (utt) (begin (Initialize utt) (POS utt)
(Phrasify utt) (Word utt) (Pauses utt)
(Intonation utt) (PostLex utt) (Duration utt)
(Int_Targets utt) (Wave_Synth utt) ))

(define (utt.synth utt)
(apply_hooks before_synth_hooks utt)
(let ((type (utt.type utt)))
  (let ((definition (assoc type UttTypes)))
    (if (null? definition)
        (error "Unknown utterance type" type)
        (let ((body (eval (cons 'lambda
                                (cons '(utt) (cdr definition))))))  (print body)
           (body utt)))))
(apply_hooks after_synth_hooks utt)
utt)

試著以自訂函數 step10 來取代上述的無名函數

(define (step10 utt) (begin (Initialize utt) (POS utt) (Phrasify utt) (Word utt) (Pauses utt) (Intonation utt) (PostLex utt) (Duration utt) (Int_Targets utt) (Wave_Synth utt) ))

再依照上述定義 step10方式,根據10個步驟再定義 step9 ~ step1 總共有10個函數,然後針對 sen 作用後再觀察其資料結構,整理如下:

  • step1 增加了 Word 結構
  • step3 增加了 Phrase 結構
  • step4 增加了Syllable , Segment ,  SylStructure 結構
  • step6 增加了 IntEvent , Intonation 結構
  • step9 增加了 Target 結構
  • step10 增加了 Unit , SourceCoef , f0 ,TargetCoef , US_map , Wave 結構
亦可利用下列指令觀察指定資料結構:
(utt.relation.item sen 'Word)
亦可利用下列指令觀察指定資料結構的詳細樹狀內容:
(utt.relation_tree sen 'Word)

在執行step9之後來利用 utt.relationnames 觀看已產生的結構 ,發現有8種結構,再以(utt.play sen)測試是否可以發聲,發現無法發出聲音,證明少掉最後一個步驟就無法發聲了

在執行step10之後來利用 utt.relationnames 觀看已產生的結構 ,發現有14種結構(增加了6個),再以(utt.play sen)測試是否可以發聲,發現果然可以發聲

festival>(step1 sen)
festival>(utt.relationnames sen)
(Word)
festival>(step2 sen)
festival>(utt.relationnames sen)
(Word)
festival>(step3 sen)
festival>(utt.relationnames sen)
(Word Phrase)
festival>(step4 sen)
festival>(utt.relationnames sen)
(Word  Phrase  Syllable  Segment  SylStructure)
festival>(step5 sen)
festival>(utt.relationnames sen)
(Word  Phrase  Syllable  Segment  SylStructure)
festival>(step6 sen)
festival>(utt.relationnames sen)
(Word  Phrase  Syllable  Segment  SylStructure IntEvent Intonation)
festival>(step7 sen)
festival>(utt.relationnames sen)
(Word  Phrase  Syllable  Segment  SylStructure IntEvent Intonation)
festival>(step8 sen)
festival>(utt.relationnames sen)
(Word  Phrase  Syllable  Segment  SylStructure IntEvent Intonation)
festival>(step9 sen)
festival>(utt.relationnames sen)
(Word  Phrase  Syllable  Segment  SylStructure IntEvent Intonation Target)
festival>(utt.play sen)
-=-=-=-=-=- EST Error -=-=-=-=-=-
{FND} Feature Wave not Defined
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

festival>(step10 sen)
festival>(utt.relationnames sen)
(Word  Phrase  Syllable  Segment  SylStructure  IntEvent  Intronation  Target  Unit SourceCoef  f0  TargetCoef  US_map  Wave)
festival>(utt.play sen)

  1. 刪掉除了Wave以外的13個資料結構
  2. 以utt.play仍然可以發聲,表示 utt.play參考 Wave_Synth 產生的6種結構中的最後一種(Wave)
  3. 可見只要執行過 Wave_Synth後 ,除了Wave這個資料結構外的改變都不會影響語音的輸出,或可說只要Wave_Synth可以正確執行,就能產生 Wave 的資料結構。
  4. 因此要探討 Wave_Synth可正確執行的條件為何?是參考哪些資料結構?

festival>(utt.relation.delete sen 'Word)
festival>(utt.relation.delete sen 'Phrase)
festival>(utt.relation.delete sen 'Syllable)
festival>(utt.relation.delete sen 'Segment)
festival>(utt.relation.delete sen 'SylStructure)
festival>(utt.relation.delete sen 'IntEvent)
festival>(utt.relation.delete sen 'Intronation)
festival>(utt.relation.delete sen 'Target)
festival>(utt.relation.delete sen 'Unit)
festival>(utt.relation.delete sen 'SourceCoef)
festival>(utt.relation.delete sen 'f0)
festival>(utt.relation.delete sen 'TargetCoef)
festival>(utt.relation.delete sen 'US_map)
festival>(utt.relationnames sen)
(Wave)
festival>(utt.play sen)

改在做 Wave_Synth步驟前, 嘗試以下的方法

  1. 刪除Word資料結構,可執行 Wave_Synth
  2. 刪除Phrase資料結構,可執行 Wave_Synth
  3. 刪除Syllable資料結構,可執行 Wave_Synth
  4. 刪除Segment資料結構,則無法執行 Wave_Synth

因此可說明 Wave_Synth 必須參考的資料結構是 Segment

 

festival>(set ! wd (Utterance Words (hello)))
festival>
(step9 wd)
festival>
(utt.relation.delete wd 'Word)
festival>(utt.relation.delete wd 'Phrase)
festival>(utt.relation.delete wd 'Syllable)
festival>(utt.relation.delete wd 'Segment)
festival>(Wave_Synth wd)
-=-=-=-=-=- EST Error -=-=-=-=-=-=-
{FND} Feature Segment not Defined
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

festival>(utt.relationnames wd)
(SylStructure  IntEvent  Intronation  Target)

重新定義變數 wd ,進行9個步驟的函數處理

除了Segment之外,嘗試是否可以刪除其後的資料結構

  1. 刪除Word資料結構,可執行 Wave_Synth
  2. 刪除Phrase資料結構,可執行 Wave_Synth
  3. 刪除Syllable資料結構,可執行 Wave_Synth
  4. 刪除SylStructure資料結構,可執行 Wave_Synth
  5. 刪除IntEvent資料結構,可執行 Wave_Synth
  6. 刪除Intronation資料結構,可執行 Wave_Synth
  7. 刪除Target資料結構,則無法執行 Wave_Synth

因此可說明 Wave_Synth 還必須參考的資料結構是 Target

festival>(set ! wd (Utterance Words (hello)))
festival>
(step9 wd)
festival>
(utt.relation.delete wd 'Word)
festival>(utt.relation.delete wd 'Phrase)
festival>(utt.relation.delete wd 'Syllable)
festival>(utt.relation.delete wd 'SylStructure)
festival>(utt.relation.delete wd 'IntEvent)
festival>(utt.relation.delete wd 'Intonation)
festival>(utt.relation.delete wd 'Target)
festival>(Wave_Synth wd)
-=-=-=-=-=- EST Error -=-=-=-=-=-=-
{FND} Feature Target not Defined
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

festival>(utt.relationnames wd)
(Segment)

觀察以上的結果,覺得可能可以不要參考到Segment結構,因此想針對Segment結構做刪減,看看是否能再簡化下去,希望能達到「只要自訂 Target 資料結構,就可以交給 Wave_Synth 來合成

刪除所有的 7 個 item 後,雖然能正確執行第10個步驟「Wave_Synth」,卻無法正確發聲,若改成只刪除4個 item後,再執行 Wave_Synth,會發覺發出的聲音被切一半,由此證明合成聲音時的確會參考到 Segment

原因是 Target 的結構與 Segment 的結構交叉參照了,若刪除所有Segment結構,則Target結構也不見了

真的要刪,就只能刪除Segment的 first 與 last 的item (pau),而看起來甚至連m與b都可以刪,但實際還是會影響
(item.delete (item.prev (utt.relation.last sen 'Segment)))

festival>(utt.relation_tree wd 'Segment)
((("pau" ((id "_10") (name "pau") (dur_factor 0) (end 0.22))))
(("s" ((id "_4") (name "s") (dur_factor 0.457137) (end 0.350805))))
(("aa" ((id "_5") (name "aa") (dur_factor 0.153892) (end 0.460469))))
(("m" ((id "_6") (name "m") (dur_factor -0.470784) (end 0.521869))))
(("b" ((id "_7") (name "b") (dur_factor -0.59933) (end 0.581946))))
(("ax" ((id "_9") (name "ax") (dur_factor 1.69917) (end 0.677405))))
(("pau" ((id "_11") (name "pau") (dur_factor 0) (end 0.897405)))))
festival>(item.delete (utt.relation.first wd 'Segment))
festival>(item.delete (utt.relation.last wd 'Segment))
festival>#.........再重複5次, 刪除所有的item..........
festival>(utt.features wd 'Segment '(id name dur_factor end))
nil
festival>(utt.relationnames wd)
(Segment Target)
festival>(Wave_Synth wd)
festival>(utt.play wd)

希望能手動產生Segment與Target兩個資料結構,可以有2個作法:
1.直接定義一個list的變數

(set ! sen '((id "_10") (name "pau") (dur_factor 0) (end 0.22) ))

2.修改舊有的結構

(item.insert (item.next utt.relation.first sen 'Segment) 'before)

3.新增Segment與Target二個結構,如右

festival>(set! w1 (Utterance Words ()))
festival>(utt.relation.create w1 'Segment)
festival>(set! tt1 (utt.relation.append w1 'Segment))
festival>(item.set_feat tt1 "id" "_10")
festival>(item.set_feat tt1 "name" "pau")
festival>(item.set_feat tt1 "dur_factor" 0)
festival>(item.set_feat tt1 "end" 0.22)
festival>(set! tt2 (utt.relation.append w1 'Segment))
festival>(item.set_feat tt2 "id" "_4")
......
(utt.relation.create w1 'Target)
(set! tt1 (utt.relation.append w1 'Target))
(item.set_feat tt1 "id" "_4")
(item.set_feat tt1 "name" "s")
(item.set_feat tt1 "dur_factor" 0.457137)
(item.set_feat tt1 "end" 0.350805)
(set! tt1_d1 (item.append_daughter tt1))
(item.set_feat tt1_d1 "id" "_14")
(item.set_feat tt1_d1 "f0" 99.8505)
(item.set_feat tt1_d1 "pos" 0.22)
(set! tt2 (utt.relation.append w1 'Target))
(item.set_feat tt2 "id" "_5")
(item.set_feat tt2 "name" "aa")
.......
(utt.relation_tree w1 'Segment)
(utt.relation_tree w1 'Target)

尚未完成......


嘗試以 segment 的層次來切入,首先看看在 lexicon 中的 diphone 為何

定義好Utterance,在合成時就發生錯誤了
不認識Segment ? 看來要走另一條路了

festival>(lex.lookup 'hello)
("hello" nil (((hh ax l) 0) ((ow) 1)))
festival>(set! sen (Utterance Segment ((hh 0.058) (ax 0.039) (l 0.069) (ow 0.219))))
festival>(utt.synth sen)
SIOD ERROR: Unknown utterance type : Segment

lexicon中的對應是否可以修改呢?

果然發出reagan的聲音喔

festival>(lex.lookup 'hello)
("hello" nil (((hh ax l) 0) ((ow) 1)))
festival>(lex.add.entry '("hello" nil ((( r ey g) 0) ((ow) 1)))))
festival>(SayText "hello")

要利用festival發出中文相當的受限,原因在於我們必須利用festival現有的 language 與 lexicon 配合才能組合出相近的聲音,但目前提供的 lexicon有US  , UK , spanish 等三種phoneset ,在載入festival之後,我們先來看看預設的lexicon並注意聆聽是美聲還是英聲,並在改變language之後,再看看其變化

festival>(PhoneSet.list)
(radio)
festival>(intro)
festival>(voice_rab_diphone)
festival>(PhoneSet.list)
(radio mrpa)
festival>(intro)
festival>(voice_don_diphone)
festival>(PhoneSet.list)
(radio mrpa holmes)
festival>(intro)

以上的範例,對於非英語系國家的我們而言是有點難以分別美式英語與英式英語的腔調的,但的確看到了PhoneSet的改變,我們再來進一步看看這些PhoneSet的詳細內容

英式英文:voice_rab_diphone

西班牙文:voice_el_diphone

奇怪的是原以為選擇對應的語言,其PhoneSet 應該也會跟著變,但實測的結果並不一定耶?

festival>(voice_rab_diphone)
(rab_diphone)
festival>(PhoneSet.list)
(radio mrpa)
festival>(PhoneSet.description)
((name mrpa) .................
festival>(voice_el_diphone)
el_diphone
festival>(PhoneSet.list)
(radio mrpa spanish)
festival>(PhoneSet.description)
((name spanish) .................

語音結構遊覽必備函數

(utt.relationnames UTT)
回傳UTT這個list中所包含的所有relations名稱
festival>(set ! sen (Utterance Words (hello)))
festival>(utt.synth sen)
(Word  Phrase  Syllable  Segment  SylStructure  IntEvent  Intronation  Target  Unit SourceCoef  f0  TargetCoef  US_map  Wave)
(utt.relation.items UTT  RELATIONNAME)
回傳UTT這個list中所有在RELATIONNAME的items指標

festival>(utt.relation.items sen 'Segment)
<#<item 0xb28a828>
  #<item 0xb289fc8>
  #<item 0xb28a168>
  #<item 0xb28a308>
  #<item 0xb28a688>
  #<item 0xb28a8e0>>

(utt.relation_tree UTT  RELATIONNAME)
回傳UTT這個list中所有在RELATIONNAME的items詳細內容,並以樹狀表示

festival>(utt.relation_tree sen 'Segment)
(((("pau"
   ((id "_9" )
   (name "pau")
   (dur_factor 0)
   (end 0.22)
   (source_end 0.081826))))
(("hh"
   (id "_4")
   (name "hh")
   (dur_factor -0.296956)
   (end 0.277954)
   (source_end 0.0188655))))
...
...

 

Festival說明檔摘要

快速上手

festival>(set! utt1 (Utterance Text "hello world")) //產生一個發音的物件儲存在utt1這個變數中
festival>(utt.synth utt1) //將utt1的物件合成波形
festival>(utt.play utt1) //將utt1的物件拿去發聲
festival>(SayText "hello world") //以上3步驟的簡化
festival>(utt.save.wave utt1 'myutterance.wav) //將utt1的物件合成波形後產生指定的wav檔
festival>(Utterance Words (hello world)) //產生一個Words的物件
festival>(Utterance Phones (pau hh ah l ow pau)) //產生一個Phones的物件
festival>(Utterance Phrase ((Phrase ((name B))I saw))) //產生一個Phrase的物件

Scheme

festival是以Scheme語法來控制,運算式是由小括號括起來的陳述式為主體

Emacs and Sable

可藉由Emacs作為介面選擇想要翻譯的區塊送給Fest ival
Sable這個標示語言以XML為基礎提供語音資料良好的格式,早期的版本是基於SSML(SpeechSynthesis Markup Language)
festival>(tts "filename.sable" 'sable ) //可唸出所指定的sable格式檔
#festival --tts filename.sable //命令模式下直接唸出指定的sable格式檔

Utterances

festival語音合成的基本物件就是utterance,以下列出如何將文字轉換為發音的步驟

  1. tokenization
  2. token identification
  3. token to word
  4. part of speech
  5. prosodic phrasing
  6. lexical lookup
  7. intonational accents
  8. assign duration
  9. generate F0 contour(tune)
  10. render waveform

以自由軟體實作應用於專屬領域的合成語音

摘要

本文介紹了一個可靠又有效率的方法來建立應用於專屬領域(limited domain)的合成語音,完整的合成語音要能夠發出所有的字詞甚至是符號,但在某些應用系統上卻只要發出部份且固定的字詞而不需要每個字都能夠發聲,像是 117 的報時台或預報天氣等系統就僅需要專屬領域的合成語音,以下讓我們來實作一套以自己的聲音錄製的報時語音檔藉著Festival語音合成工具與Festvox聲音建立工具。

簡介

語音合成又名為文句轉語音 (Text-to-Speech),它可應用的範圍相當廣泛,例如,氣象或報時等訊息發佈系統、電話查號系統、盲胞語音輔助系統 、有聲書等,隨著進來語音應用需求的增加可以發現,大多數的語音仍舊是採取預先錄製的方法而不使用語音合成系統來產生語音,其主要原因在於, 無論接受的是一段文字的輸入或是一篇文章,這些文字本身並沒有包含任何聲學特性 (acustic feature,例如聲調的高低,停頓方式,發音長短等韻律 參數),只有語言學的特性,所以必須透過自動預測的機制來產生這些文字的可能的聲學特性 而所謂自動預測的機制,一般有 rule-based 跟 data driven 兩大類手法,但是這兩種手法合成的品質音色平淡又缺乏吸引力且遇到連續發音或要保留語者音色時表現都不好, 因此近來流行音節串接法(corpus-based syllable concatenation),就是以一個錄好聲音的語料庫來當作比對的標的,從語料庫中抓出相對應的合成單元,相較於在 rule-based 跟 data driven 手法下所需要做細節的聲韻調整也因此減少了許多,如此一來不但具有了與預錄方式一樣自然度之外更減少了大量錄製所耗費的時間與成本。

本文使用了英國愛丁堡大學語言研究中心所研發的語音合成軟體Festival,它使用了二種增進合成語音品質的技術,語音單元選取合成法(unit selection synthesis)與泛用型雙音素合成法(general diphone synthesis),而配合專屬領域的語音少量而固定變化的特性,在本篇所使用到的語音單元選取合成法是簡化過了的,方法是事先將所需要的語音錄製下來,語音合成時會自動選取其中相似的語音單元來作發聲,因此與泛用型的語音單元選取合成語音系統(general unit-selection based TTS)比較起來,聲音更為流暢與自然。

規劃製作語音環境

在製作報時語音檔前,我們要先作一些準備工作

  1. 尋找適當的語者:
    因為語者的音色會影響到合成語音的品質,同時讓語者瞭解合成的語音資料庫的授權是自由的
  2. 尋找品質好的音效卡與麥克風:
    因為我們要建立一個可以讓單元選取技術利用的資料庫
  3. 安裝下列相關套件:
    安裝與基本使用方式可參考 http://www2.cyut.edu.tw/~s9154610/festival.html#install
    1. 安裝語音合成工具:festival
    2. 安裝語音製作工具:festvox
    3. 安裝語音分析工具:emulabel
  4. 安裝位置:
    /home/peter/projects/(根據語者名稱安裝在其家目錄的projects子目錄中,本例的語者名稱為peter)
  5. 建立相關的環境變數:
    export PATH=$PATH:/home/peter/projects/festival/bin
    export FESTVOXDIR=/home/peter/projects/festvox
    export ESTDIR=/home/peter/projects/speech_tools
  6. 測試錄音效果與品質:
    我們選擇在個人電腦搭配Linux作業系統來進行錄音,缺點是品質可能會受到電子訊號影響,優點是控制較為便利。 有幾個減少噪音干擾的方法
    1.避免麥克風的線與其他的線纏繞,特別是電源線。
    2.改變電腦的角度與位置。
    3.改變音效卡的插槽位置。
  7. 耐心與恆心:
    要錄製令人滿意的語音有時候要花相當多的時間,儘量是一次錄完,因為每個人不同時段的嗓子狀態都不同,尋找相同的錄音環境與條件或心情,將所有設備調整測試到最佳狀態才能達到最好的語音品質。

建立語音

建立報時的資料庫

首先我們在使用者家目錄中建立存放語音的目錄 data/time,然後透過festvox提供的命令稿(scheme file)讓我們可根據應用的「機構」,「語者」,「領域」的參數建立在語音目錄下所需的資料庫

mkdir -p ~/data/time
cd ~/data/time
$FESTVOXDIR/src/ldom/setup_ldom cyut time peter

執行 setup_ldom 這個命令稿後我們可以發現它在 data/time 下產生了許多為了存放合成語音的目錄與檔案,特別的是在 data/time/festvox 子目錄下有4個用來控制 festival 的 scheme 命令稿,我們稍後可能需要編輯它。(檔案變動列表)

文後所提及之目錄名稱,除非特別指定,否則皆以 ~/data/time 為相對目錄起始點。

檔案變動列表利用 ls -1R 取得目錄列表。

設計提示語(Designing the prompts)

根據應用的領域我們必須設計一個提示語的參考檔,通常是以 DOMAIN.data 命名,如本例應用在語音領域就應該取名為 time.data 這個提示語的參考檔必須放在語音目錄的etc下,但因為我們使用了 festvox 提供的自製報時語音命令稿(下面例子的 build_ldom.scm ),因此 etc/time.data 已被自動建立,值得注意的是若我們要自行產生 DOMAIN.data ,其中不可有空行,且左括號之後要空一格。

( time0001 "The time is now, ..." )
( time0002 "The time is now, ..." )
( time0003 "The time is now, ..." )
.......
........
( time0024 "The time is now, ..." )

根據提示語錄製語音(Recording the prompts)

festival提供了一個命令稿 build_ldom.scm 來根據 etc/time.data 產生錄製語音時所用的提示語,它將會在 prompt-lab, prompt-utt, prompt-wav 等3個目錄中產生 time0001~time0024 各24個檔案。(檔案變動列表)

festival -b festvox/build_ldom.scm '(build_prompts "etc/time.data")'

接下來需鍵入 prompt_them 指令, festival 會根據已建立的提示語念出句子來,當我們看到"starting recording"後就跟著念一遍,festival會將語者所發的語音儲存在wav目錄中產生time0001~time0024等24個wav檔,要注意的是超過10秒的語音會花費很多的swap空間。(檔案變動列表)

bin/prompt_them etc/time.data

自動比對提示語(Autolabeling the prompts)

鍵入 make_labs 指令,將錄製好的語音比對提示語,產生的結果會在cep,lab,prompt-cep等3個目錄中有time0001~time0024各24個檔案。(檔案變動列表)
bin/make_labs prompt-wav/*.wav
接下來需鍵入 build_ldom 指令,在festival/utts目錄下產生time0001.utt~time0024.utt等24個檔案。(檔案變動列表)
festival -b festvox/build_ldom.scm '(build_utts "etc/time.data")'

萃取音調符號與建立LPC餘數(Extracting pitchmarks and building LPC coefficients)

從聲波(wav)中取出好的音調符號(pitchmark)對於合成品質的改善有很大的幫助,在pm目錄下產生time0001.pm~time0024.pm等24個檔案。(檔案變動列表)
bin/make_pm_wave wav/*.wav
針對抓出來的pitchmark 做調整以增進品質,但不會產生新的目錄或檔案(檔案變動列表)
bin/make_pm_fix pm/*.pm
正規化,產生wavn目錄並產生time0001.wav~time0024.wav等24個檔案。(檔案變動列表)
bin/simple_powernormalize wav/*.wav
產生音調同步參數化,在mcep目錄下產生time0001.mcep~time0024.mcep等24個檔案。(檔案變動列表)
bin/make_mcep wav/*.wav

從發音中取出語音合成單元(Building a clunit based synthesizer from the utterances)

取出的合成單元包含了語言學(phonetic)與聲韻學(prosodic)的特性來建立決策樹(decision tree)。

若不算錄音的時間,大約只要3分鐘的時間就可以建立完成了。

也可以使用英語之外的語言建立報時所用的語音檔喔。

(檔案變動列表)

festival -b festvox/build_ldom.scm '(build_clunits "etc/time.data")'

測試與調整(Testing and tuning)

啟動festival載入新聲音
festival festvox/cyut_time_peter_ldom.scm '(voice_cyut_time_peter_ldom)'
唸出現在時間
(saytime)
唸出指定時間
(saythistime "11:23")

參考文獻

  1. Black, A., and Lenzo, K. Building voices in the Festival speech synthesis system. http://festvox.org, 2000
  2. Black, A., and Lenzo, K. Limited Domain Synthesis.

說明檔翻譯(未完成)

相關名詞

愛丁堡藝術節

始於1947年的一個國際性藝術(特別是音樂和戲劇)節日,每年8月後第3個星期在蘇格蘭的愛丁堡舉行。1950年以前由賓(R. Bing)主持。現在的藝術指導是鄧洛普(F. Dunlop)。除藝術節的正式活動外,「臨時增加節目」日益增多,藝術節生動活潑。

CSTR(The Centre for Speech Technology Research)

CSTR是一個有關各種學問的研究中心,目前著手語音研究的應用導向

網路資源

主 網 站:http://peterju.notlong.com (目前轉址至 http://irw.ncut.edu.tw/peterju/) Sitetag Logo

Level Triple-A conformance icon | [歡迎使用任何作業系統、瀏覽器觀看!] | Valid XHTML 1.0 Transitional | Valid CSS! | [Valid RSS] | [創意公眾許可証]
This work is licensed under a Creative Commons License