如果角色有聲音的話有多好。當然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在使用上可能有的不同功能。程式如下:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) |
主要連結的語法是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的積木描述檔如下:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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] | |
} | |
} |
在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伺服程式的寫法:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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的話,總是會有個少了什麼的感覺吧!
沒有留言:
張貼留言