近日做了个文本转语音的小项目,主功能接百度智能云的API很顺利,但3个小细节处理很费了些事。就像生活中往往一些不起眼的小角色更狠、更能给人添堵,做项目也是一样。特分享记录一下,先看看项目界面和运行效果:
主功能:在顶部输入框中输入英语单词或语句,点击右侧的三个按钮,识别输入的文本,转换为语音,并进行播放。读单词模式时,中间灰色区域,同步显示英语单词和中文翻译。
1、主功能文本转语音试了三种方法,感觉百度智能云的语音技术甩别的几条街,不知是不是百度曾经的all in ai留下的战果,翻译也就顺带用他家的了。
2、通过“文件导入”功能导入文本时,打开文本、读取文本都不难,但遇到无法解码的特殊字符,只一个就导致读取失败,字符解码的细节容易被忽略,却常常出来惹事。
3、“读单词”模式,通过分割符将文本分割成一个个单词,再逐个进行语音转换,播放。但由于playsound播放文件后,资源不会被放弃,导致无法逐个播放,这是本项目最硬的茬,通过修改源库代码方解决。
4、“读单词”模式,播放语音时,中间同步显示英语和翻译,实际却是等全部读完了,才显示最后一个单词及翻译,后通过添加线程解决。
详细过程:
一、主功能,接百度语音技术解决文本转语音。
首先,需要注册百度智能云,添加语音技术应用,获得ID、key,如下图:
接下来写码就ok了。
def text_to_voice(str, cs):
APP_ID = '************************'
API_KEY = '************************'
SECRET_KEY = '************************'
client = AipSpeech(APP_ID, API_KEY, SECRET_KEY)
result = client.synthesis(str, 'zh', 1, cs)
if not isinstance(result, dict):
with open('auido.mp3', 'wb') as f:
f.write(result)
if __name__ == "__main__":
cs = {'vol': 5, 'per': 1, 'spd': 8}
text_to_voice('goodmorning', cs)
代码很简单,头三行***为从前面获得的ID、key。‘str’为要读取的文本内容,‘cs’为播放声音的参数,'vol'音调,'per'1为男声,0为女声,'spd'为语速。作为参数带入函数块,利于界面交互。
二、文件导入写入文本
def open_file():
global word
path = './'
filename = filedialog.askopenfilename(
title=u'选择文件',
filetypes=[("记事本", ".txt")],
initialdir=(os.path.expanduser(path)))
if filename != '':
f = open(filename, 'rb')
lines = f.readlines()
n = 1
for line in lines:
line = line.strip().decode('utf-8')
if line != '':
word = word + line
n += 1
e.delete(0, 'end')
e.insert(0, word)
e.delete(0, 'end')为删除文本框原内容
e.insert(0, word)将word里的内容显示在文本框里。
读取生成word内容时,line = line.strip().decode('utf-8')用decode解码,可以应付多数情况了。
三、解决不能循环播放mp3文件的问题
导入模块from playsound import playsound
直接播放playsound('auido.mp3')
但在for循环里批量播放时报错,因为playsound库源码里没有关闭文件的代码,从网上借鉴到的做法,修改源码,增加关闭功能。
打开python安装路径,找到playsound.py文件打开,
在下面的位置,增加红框内的代码
增加的代码为:
while True:
if winCommand('status', alias, 'mode').decode() == 'stopped':
winCommand('close', alias)
break
保存后,再循环运行playsound('auido.mp3')就没问题了。
将一组单词逐个进行阅读的代码:
def read_text(text):
cs['vol'] = s1.get()
cs['spd'] = s2.get()
cs['per'] = v.get()
ttv.text_to_voice(text, cs)
playsound('auido.mp3')
def one_by_one():
tt = re.split(',|.|?|。|,', content.get())
if tt != '':
for t in tt:
if t != '':
label22['text'] = t.strip() + '' + '' + en_to_zh.fy(t)
read_text(t)
time.sleep(1)
四、多线程同步显示单词及翻译
上面label22['text'] = t.strip() + '' + '' + en_to_zh.fy(t)中en_to_zh.fy(t)即为翻译的中文,首先导入自己写的含翻译功能py文件en_to_zh,再利用文件里的fy函数块进行翻译得到中文。en_to_zh.py代码为百度翻译api提供的代码,只修改为自己的id和key,然后设置return语句返回获得的中文即可:
import http.client
import hashlib
import urllib
import random
import json
def fy(txt):
appid = '*************************' # 填写你的appid
secretKey = '*************************' # 填写你的密钥
httpClient = None
myurl = '/api/trans/vip/translate'
fromLang = 'en' #原文语种
toLang = 'zh' #译文语种
salt = random.randint(32768, 65536)
q = txt#'apple'
sign = appid + q + str(salt) + secretKey
sign = hashlib.md5(sign.encode()).hexdigest()
myurl = myurl + '?appid=' + appid + '&q=' + urllib.parse.quote(
q) + '&from=' + fromLang + '&to=' + toLang + '&salt=' + str(
salt) + '&sign=' + sign
try:
httpClient = http.client.HTTPConnection('api.fanyi.baidu.com')
httpClient.request('GET', myurl)
# response是HTTPResponse对象
response = httpClient.getresponse()
result_all = response.read().decode("utf-8")
result = json.loads(result_all)
return result['trans_result'][0]['dst']
except Exception as e:
print(e)
finally:
if httpClient:
httpClient.close()
在原代码里仅加了“return result['trans_result'][0]['dst']”语句就好。该处的id和key是百度翻译里获取的,与前面的语音技术id是两码事,但注册操作方法一样。
上面获得了内容,但我们需要在for循环里动态显示在label框里,却无法实现,只能在for循环完成后,再显示最后一次的结果,显然这不是我们要的。这是因为单线程不能同时干两件事导致的,解决方法是增加线程,让同时处理两件事。
在按钮“读单词”的触发程序块里设置增加线程的代码:
def read_one():
read_text('请跟我读:')
t = threading.Thread(target=one_by_one, args=(), name='thread-refresh')
t.setDaemon(True)
t.start()
真正的运行阅读代码写在函数one_by_one()里。
五、交互界面代码
为丰富播放体验,设置了单选按钮、数据拉条进行CS参数设置,具体过程不细讲,代码附上:
if __name__ == "__main__":
cs = {'vol': 8, 'per': 0, 'spd': 4}
root = tkinter.Tk()
root.title("英语领读")
root.geometry('660x420-20+30')
# 第0行
ft0 = tkFont.Font(family='楷体', size=15)
titlelabel = tkinter.Label(root, text='输入或通过文件导入阅读内容', width=40, font=ft0, anchor='s')
titlelabel.grid(row=0, column=0, columnspan=6, pady=10, sticky='S')
# 第1行
pic_11 = tkinter.PhotoImage(file='pic/99.png')
bt11 = tkinter.Button(root, text='从文件导入', image=pic_11, width=65, height=25, command=open_file)
bt11.grid(row=1, column=0, pady=8, padx=5, columnspan=1, sticky='NW')
content = tkinter.StringVar()
e = tkinter.Entry(root, textvariable=content, font=ft0, width=46)
e.grid(row=1, column=1, columnspan=4, pady=8, sticky='NW')
content.set('hai, nice to meet you!')
pic_1 = tkinter.PhotoImage(file='pic/12.png')
bt12 = tkinter.Button(root, text='清空', width=8,
command=clear) #, image=pic_1
bt12.grid(row=1, column=5, pady=8, padx=5)
# 第2行
ft = tkFont.Font(family='楷体', size=38, weight=tkFont.BOLD)
label22 = tkinter.Label(root,
width=17,
height=5,
bg='DarkGray',#DarkSeaGreen DarkGray
fg='black',
justify='left',
relief='sunken',
font=ft) #wraplength=120,
label22.grid(row=2, column=1, rowspan=3, columnspan=4, sticky='NW')
# 第3行
pic_31 = tkinter.PhotoImage(file='pic/one11.png')
bt31 = tkinter.Button(root,
text='读单词',
width=60,
image=pic_31,
command=read_one)
bt31.grid(row=2, column=5, padx=15)
pic_32 = tkinter.PhotoImage(file='pic/all11.png')
bt32 = tkinter.Button(root,
text='读整段',
width=60,
image=pic_32,
command=read_all)
bt32.grid(row=3, column=5, padx=15)
pic_33 = tkinter.PhotoImage(file='pic/tx1.png')
bt33 = tkinter.Button(root,
text='听写',
width=60,
image=pic_33,
command=dictation)
bt33.grid(row=4, column=5, padx=15)
ft2 = tkFont.Font(family='Fixdsys', size=8)
label8 = tkinter.Label(root,
text='版本: bmy-001',
width=20,
font=ft2)
label8.grid(row=5, column=5)
s1 = tkinter.Scale(root,
label='音调',
from_=0,
to=10,
orient=tkinter.HORIZONTAL,
length=200,
showvalue=0,
tickinterval=10,
resolution=1)
s1.grid(row=5, column=1, columnspan=2, padx=15)
s1.set(5)
s2 = tkinter.Scale(root,
label='语速',
from_=0,
to=10,
orient=tkinter.HORIZONTAL,
length=200,
showvalue=0,
tickinterval=10,
resolution=1)
s2.grid(row=5, column=3, columnspan=2, padx=15, pady=1, sticky='N')
s2.set(5)
v = tkinter.IntVar()
boy = tkinter.PhotoImage(file='pic/男1.png')
tkinter.Radiobutton(
root,
text='男声',
variable=v,
image=boy,
value=1,
).grid(row=2, column=0, sticky='S')
girl = tkinter.PhotoImage(file='pic/女1.png')
tkinter.Radiobutton(
root,
text='女声',
variable=v,
image=girl,
value=0,
).grid(row=3, column=0)
v.set(0)
photo = tkinter.PhotoImage(file="./pic/古代儿童.png") # file:t图片路径
imgLabel = tkinter.Label(root, image=photo)
imgLabel.grid(row=4, column=0, rowspan=2, sticky='S')
root.mainloop()
六、预留功能:随时暂停播放过程的功能暂未设置,因为用生涩的底层控制代码去实现一个普通不过的mp3文件暂停播放功能,感觉性价比真的好差,所以......