2015年8月17日 星期一

用Python擴充我的Scratch2 -- 文字轉語音

在使用Scratch做動畫時,常常要呈現角色的台詞,有時在思考,
如果角色有聲音的話有多好。當然Scratch是可以做到,只要把台
詞一句一句錄成聲音檔就好了,只是過程會很辛苦。那有沒有什麼
方法可以讓角色的發出聲音呢?記得之前在寫AppInventor時,
有使用到一種依照文字動態、即時地發出聲音的做法,就是「文字轉語音」
(Text To Speech,簡稱TTS)。


(圖片來源:minionvilla.com)

「文字轉語音」的方式在不同的OS上許多選擇,在IOS上有NSSpeechSynthesizer
,在linux有espeak,在Windows有SAPI5,google也有線上的TTS引擎可以使用, 甚至也有「工研院文字轉語音Web服務」,因為自己只以手邊方便的做法,所以只有嘗試SAPI5,其他的TTS方法我目前都未研究,有興趣的人都可以試試看,不過要注意是否支援中文語音。


在這裡提一下我的實作環境,OS是Windows8.1的筆電,python是3.4版,使
用的SAPI5的TTS功能,為了能存取SAPI的TTS功能(SpVoice的com物件),
python需要安裝win32模組。對於OS,TTS,或是程式語言,只要能找到同樣
功能的組合,也不一定要跟我的環境一樣,只有原理了解,找到合適工具,
相信也可以有不同的做法來結合TTS的功能。


這一篇文章主要討論的是,使用python,依照Scratch2的擴充積木機制,
,做出Scratch2的「文字轉語音積木」(SAPI TTS),有關Scratch2的擴充
原理,請參考之前的文章。本文會在原理的基礎上,做一些應用。有關
SAPI5的TTS(SpVoice)的參考資料,詳細請看MSDN

要讓Scratch2支援文字轉語音(TTS),主要有三個問題,第一個是python
要如何使用SAPI5的TTS功能。第二個是TTS的積木描述檔(s2e)要怎麼定義。
第三個是helper伺服程式要怎麼寫。沒關係,就一個一個來克服吧!

關於第一個問題,python要使用SAPI5的TTS功能,需要安裝pywin32模組,因安裝時有地方要特別注意,所以稍做一下說明。下載連結是 pywin32 Source Forge ,要注意要下載的檔案,必須要與python的版本相對應,在下圖是我python的互動模式

在兩個紅框中的資訊,表示我的python是3.4版,以32位元編譯(雖然我的機器是64位元),所以下載檔案時,要下載如下圖紅框標示的檔案,下載完就進行安裝

那python使用SAPI5的TTS,要怎麼寫呢?我寫了一個測試程式,測試TTS在使用上可能有的不同功能。程式如下:

#!/usr/bin/env python3
import time
import win32com.client
speaker = win32com.client.Dispatch("SAPI.SpVoice") #連接SAPI
SVSFDefault = 0
SVSFlagsAsync = 1
speaker.Volume = 100
speaker.Rate = 0
s = "S Four A蘇老師人人誇,熱心投入創客教育,指導學生非常用心"
speaker.Speak("測試一,語音等待測試")
speaker.Speak(s, SVSFDefault)
print ("此段文字在語音結束後印出")
time.sleep(2)
speaker.Speak("測試二,語音不等待測試")
speaker.Speak(s, SVSFlagsAsync)
print ("此段文字與語音同時印出")
speaker.WaitUntilDone(-1)
time.sleep(2)
speaker.Speak("測試三,語音音量測試,分為大、中、小")
speaker.Volume = 100
speaker.Speak("S Four A蘇老師人人誇", SVSFDefault)
speaker.Volume = 85
speaker.Speak("熱心投入創客教育", SVSFDefault)
speaker.Volume = 70
speaker.Speak("指導學生非常用心", SVSFDefault)
time.sleep(2)
speaker.Volume = 100
speaker.Speak("測試四,語音速度測試,分為中、快、慢")
speaker.Rate = 0
speaker.Speak("S Four A蘇老師人人誇", SVSFDefault)
speaker.Rate = 6
speaker.Speak("熱心投入創客教育", SVSFDefault)
speaker.Rate = -9
speaker.Speak("指導學生非常用心", SVSFDefault)
speaker.WaitUntilDone(-1)
view raw sapi_tts_py3.py hosted with ❤ by GitHub

主要連結的語法是3~6行,產生出來的speaker物件,可以透過它就可以做出TTS功能。在第12及13行中,Rate是改變語音的速度(-10~10),Volume是語音音量(0~100),真正要說出語音,是用像19行的寫法,speaker.Speak("要說的文字") ,這樣就可以把語音說出來。在這個程式中,我用了第15行的示範文字(使用蘇恆誠老師的名義,請見諒),分別做了四個測試,因為也會與後面的Scratch2積木功能設計有關,所以在此說明一下。

測試一是同步語音(19~21行),會等待聲音播完後,再執行下一行

測試二是非同步語音(25~30行),不會等待聲音播完,程式會繼續執行下去

測試三是語音音量(35~42行) ,有大、中、小聲

測試四是語音速度(47~53行)  ,中、快 、慢速

程式實際運行的影片如下(主要是聽語音的部分):



接著第二個問題,在Scratch2中,SAPI TTS積木要怎麼規畫呢?大致上是依照之前的四個測試,來設計出四個積木,s2e的積木描述檔如下:
{
"extensionName": "文字轉語音(50555)",
"extensionPort": 50555,
"blockSpecs": [
[" ", "%s 轉語音", "ttsNoWait", "文字"],
["w", "%s 轉語音並等待", "ttsWait", "文字"],
[" ", "語音速度 %m.speed", "voiceSpeed", 0],
[" ", "語音音量 %m.volume", "voiceVolume", 100]
],
"menus" : {
"speed" : [10,9,8,7,6,5,4,3,2,1,0,-1,-2,-3,-4,-5,-6,-7,-8,-9,-10],
"volume" : [100,90,80,70,60,50,40,30,20,10,0]
}
}
view raw tts_helper.s2e hosted with ❤ by GitHub

 在Scratch2中匯入s2e檔之後的積木如下圖:

這次的s2e檔定義了四個新積木,比上次的再複雜一些,解釋如下
(每個積木的前三個參數說明,請看前一篇)

第一個積木的定義是在 s2e檔中的第6行,這是不等待的轉語音積木,跟上次比起來,第2個參數裡的積木格式,多了%s,這會讓積木中有個文字格子。另外多了第4個參數 "文字" ,這是代表的是積木裡格子的預設值

第二個積木的定義是在第7行,這是會等待的轉語音積木, 語音播完 ,Scratch2才會繼續下一個積木。在參數定義中,第1個積木種類參數是"w",這是等待積木的意思,Scratch2會等這個積木執行完。(註:積木等待執行的機制其實有另外的細節,可參考Scratch2的資料)  

(等待與不等待這兩種積木,在原本的Scratch2就有使用,如「播放聲音」與「廣播」積木)

第三個積木的定義是在第8行,這個積木會把語音速度值送出。在第二個積木格式參數,有個沒見過的%m.speed,其中的%m會讓積木上出現選單,而speed是會到第13行去找出選單的內容,在這裡語音速度值是從-10到10

第四個積木的定義是在第9行,這個積木會把語音音量值送出。同樣%m.volume產生一個選單,值的範圍是0~100    

到這裡完成了s2e的積木描述檔

最後一個問題,helper伺服程式如何寫呢?基本上只要把第一個測試的python程式,加上Scratch2的helper伺服程式,就可以了,先來看一下helper伺服程式的寫法:
#! /usr/bin/env python3
from http.server import BaseHTTPRequestHandler
from http.server import HTTPServer
from urllib.parse import quote, unquote
import os, sys
import win32com.client
###### 全域變數建議區 #####
####################################################
HELPER_NAME = "SAPI文字轉語音"
HELPER_PORT = 50555
VOICE_RATE = 0 #語音速度
VOICE_VOLUME = 100 #語音音量
SVSFDefault = 0
SVSFlagsAsync = 1
####################################################
speaker = win32com.client.Dispatch("SAPI.SpVoice")
speaker.Rate = VOICE_RATE
speaker.Volume = VOICE_VOLUME
class CmdHandler(BaseHTTPRequestHandler):
"""
This class handles HTTP GET requests sent from Scratch2.
"""
def do_GET(self):
"""
process HTTP GET requests
"""
# skip over the first / . example: /poll -> poll
cmd = self.path[1:]
# create a command list .
cmd_list = cmd.split('/')
s = "不回傳資料"
###### 處理Scratch2送出的命令
###### 若需回應Scratch2的Poll命令,再把文字存在變數s ##
##############################################################
#轉語音
if cmd_list[0] == "ttsNoWait" :
voice_text = unquote(cmd_list[1])
speaker.Speak(voice_text,SVSFlagsAsync)
#轉語音(直到完畢)
if cmd_list[0] == "ttsWait" :
voice_text = unquote(cmd_list[2])
speaker.Speak(voice_text,SVSFDefault)
#語音速度( -10 ~ 10 )
if cmd_list[0] == "voiceSpeed" :
speaker.Rate = int(cmd_list[1])
#語音音量( 100 ~ 0 )
if cmd_list[0] == "voiceVolume" :
speaker.Volume = int(cmd_list[1])
#############################################################
self.send_resp(s)
def send_resp(self, response):
"""
This method sends Scratch an HTTP response to an HTTP GET command.
"""
crlf = "\r\n"
http_response = "HTTP/1.1 200 OK" + crlf
http_response += "Content-Type: text/html; charset=ISO-8859-1" + crlf
http_response += "Content-Length" + str(len(response)) + crlf
http_response += "Access-Control-Allow-Origin: *" + crlf
http_response += crlf
if response != '不回傳資料':
http_response += str(response + crlf)
# send it out the door to Scratch
self.wfile.write(http_response.encode('utf-8'))
def start_server():
"""
This function populates class variables with essential data and
instantiates the HTTP Server
"""
try:
server = HTTPServer(('localhost', HELPER_PORT ), CmdHandler)
print ('啟動<' + HELPER_NAME + '>伺服程式!(port ' + str(HELPER_PORT) + ')')
print ('要退出請按 <Ctrl-C> \n')
print ('請執行Scrath2(記得要開啟對應的s2e檔案!)')
except Exception:
print ('HTTP Socket may already be in use - restart Scratch')
raise
try:
#start the server
server.serve_forever()
except KeyboardInterrupt:
print ('\n\n退出程式……\n')
sys.exit()
if __name__ == "__main__":
start_server()

第11~26行是建議的全域變數區域,也可以不放這裡,但放在此處比較好理解。
全域變數有語音速度、語音音量

第29~31行是要呼叫SAPI,產生一個speaker物件(可與第一個測試程式的code互相對照)

再來http的程式一樣略過,主要來看53~77之間,處理Scratch2送來的Get 命令的程式碼。這裡的四段if,就會分別處理 s2e中定義的四個積木命令名稱的真正動作,實際的轉成語音,改速度,音量,都是在這個地方完成。(可與第一個測試程式的code互相對照)

在這邊特別提出的是,當積木有參數時,實際送出的命令是什麼呢?以第一個積木為例 ,如果第一個積木的文字是Hi, 那送出的命令是/ttsNoWait/Hi,參數會在命令名稱後面,加上/後,接上文字內容。另外要提的是,如果參數是中文的話,會像中文網址一樣,會做URL的編碼,所以在59與64行,為什麼會加上unquote函式,就是要把URL編碼過的中文字再還原回來。(註:有認真看code的人會發現,第64行有點不一樣,cmd_list的索引值變成2,這個主要是因Scratch2在處理等待積木,會自動多加上一個參數的關係)

最後完成的Scratch2 的SAPI TTS擴充積木的測試影片,請看下方


這樣就可以在Scratch2中使用「文字轉語音TTS」的功能了。

最後附一段這篇文章的概念影片,影片的最後,有個應用於動畫的效果展示


做了半天,寫了這麼久,終於完成了。有時會問自已,這麼麻煩的做出這個功能,值得嗎?等別人做出來不就好了?到底是為什麼?

也許是覺得有趣,或許是覺得很酷,亦或是想要了解事物運作原理的好奇心吧!還有,不管是maker或是Coder,如果我自己如果不動手making 或是coding的話,總是會有個少了什麼的感覺吧!




沒有留言:

張貼留言