近日做了个文本转语音的小项目,主功能接百度智能云的API很顺利,但3个小细节处理很费了些事。就像生活中往往一些不起眼的小角色更狠、更能给人添堵,做项目也是一样。特分享记录一下,先看看项目界面和运行效果:




Python语音转文字 源码 python文字转语音库_for循环


Python语音转文字 源码 python文字转语音库_Python语音转文字 源码_02


主功能:在顶部输入框中输入英语单词或语句,点击右侧的三个按钮,识别输入的文本,转换为语音,并进行播放。读单词模式时,中间灰色区域,同步显示英语单词和中文翻译。

1、主功能文本转语音试了三种方法,感觉百度智能云的语音技术甩别的几条街,不知是不是百度曾经的all in ai留下的战果,翻译也就顺带用他家的了。

2、通过“文件导入”功能导入文本时,打开文本、读取文本都不难,但遇到无法解码的特殊字符,只一个就导致读取失败,字符解码的细节容易被忽略,却常常出来惹事。

3、“读单词”模式,通过分割符将文本分割成一个个单词,再逐个进行语音转换,播放。但由于playsound播放文件后,资源不会被放弃,导致无法逐个播放,这是本项目最硬的茬,通过修改源库代码方解决。

4、“读单词”模式,播放语音时,中间同步显示英语和翻译,实际却是等全部读完了,才显示最后一个单词及翻译,后通过添加线程解决。

详细过程:

一、主功能,接百度语音技术解决文本转语音。

首先,需要注册百度智能云,添加语音技术应用,获得ID、key,如下图:


Python语音转文字 源码 python文字转语音库_for循环_03


接下来写码就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文件打开,


Python语音转文字 源码 python文字转语音库_百度_04


在下面的位置,增加红框内的代码


Python语音转文字 源码 python文字转语音库_Python语音转文字 源码_05


增加的代码为:

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循环完成后,再显示最后一次的结果,显然这不是我们要的。这是因为单线程不能同时干两件事导致的,解决方法是增加线程,让同时处理两件事。

在按钮“读单词”的触发程序块里设置增加线程的代码:


Python语音转文字 源码 python文字转语音库_亚索全部语音原声mp3_06

bt31 = tkinter.Button(root, ... command=read_one)
  
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文件暂停播放功能,感觉性价比真的好差,所以......