Python利用讯飞语音API实现方言学习之语音接口封装
前言:前一篇完成了语音接口的封装,接下来完成PyQt5界面实现。
Qt Creator界面设计
PyQt使用非常顺畅,因为Qt提供了跨平台的整套解决方案,采用进行Qt Creator一站解决开发者全部需求,包括编码,帮助文档,界面设计。
软件主要分成两部分界面,简单文档编辑界面和合成的WAV播放器(暂未实现),分成上下两个部分,在界面呈现时会隐藏其中一个界面,并通过 模式 按钮切换界面。
ui文件转为py文件
安装了PyQt5之后就会在Python环境安装pyuic5.exe,通过这个工具,我们可以直接通过命令将ui文件转为py文件。
pyuic5 命令选项
Usage: pyuic5 [options] <ui-file>
Options: --version show program's version number and exit -h, --help show this help message and exit -p, --preview show a preview of the UI instead of generating code -o FILE, --output=FILE write generated code to FILE instead of stdout -x, --execute generate extra code to test and display the class -d, --debug show debug output -i N, --indent=N set indent width to N spaces, tab if N is 0 [default: 4] Code generation options: --import-from=PACKAGE generate imports of pyrcc5 generated modules in the style 'from PACKAGE import ...' --from-imports the equivalent of '--import-from=.' --resource-suffix=SUFFIX append SUFFIX to the basename of resource files [default: _rc]
将 voice_ui.ui 转为 voice_ui.py
pyuic5 -o voice_ui.py voice_ui.ui
生成的文件py文件,主要包含 setupUi 和 retranslateUi 函数
界面类VoiceWidget实现
在生成的界面py文件基础上需要一个框架来承载它,这里就是setupUi参数项中的MainForm参数。因此需要自己写一个继承自Qt界面类的类来完成界面逻辑操作。
class VoiceWidget(QtWidgets.QWidget):
__CONFIG_FILE = 'config.json'
def __init__(self):
super().__init__()
self._select_docpath = None
self.ui = Ui_MainForm()
self.ui.setupUi(self)
self.docs = Document()
self.selected_workdir(os.path.abspath('./workdir'))
self.taskmgr = TaskManager(self)
self.restore_ui()
self.show()
self.ui.dock_music_widget.hide()
def restore_ui(self):
pass
def save_ui(self):
pass
def clean(self):
pass
...
界面类主要处理按钮,滑块,列表,编辑器的事情。窗口设计在Qt Creator中已经完成。
界面采用组合设计,各个组件,Document,TaskManager生命周期与界面一致。
创建窗口
def main():
app = QtWidgets.QApplication(sys.argv)
m = VoiceWidget()
m.setWindowTitle('Learn')
ret = app.exec_()
m.clean()
sys.exit(ret)
信号连接
实际上信号连接可以通过Qt Cretor的 *Signals & Slots Editor 实现编辑,然后稍加修改,比如:
处理Button按钮的press信号:
self.save_pushButton.pressed.connect(MainForm.on_clicked_save)
处理文件列表的右键信号:
self.file_listWidget.itemClicked['QListWidgetItem*'].connect(MainForm.on_clicked_listwidgetitem)
处理水平滑块滑动信号:
self.voice1_horizontalSlider.valueChanged['int'].connect(MainForm.on_speed_changed)
处理语音选择下拉框信号:
self.voice1_comboBox.currentIndexChanged['int'].connect(MainForm.on_people_changed)
QtListWidget 文本不能动态显示bug修复
c++版本qt也存在该bug
bselect = item.isSelected()
self.ui.file_listWidget.takeItem(idx)
self.ui.file_listWidget.insertItem(idx, item)
item.setSelected(bselect)
文档类Document实现
Document类主要实现工作目录维护,txt和wav之间的状态关系。
def __init__(self):
self.curdoc = None
self._workdir = None
self.alldocs = []
主要维护当前文档curdoc ,所有alldocs。工作目录,所有的信息都是在来自于工作目录,通过扫描工作目录建立信息组织。工作目录包含:cache、text、wav三个目录。
每一doc项包含一个词典
DOCSTATE = ['NORMAL', 'WAITING', 'DOING', 'DONE', 'FAIL']
doc = {
'docpath': doc,
'wavpath': wav,
'wavstatus': '',
'docstatus': 'NORMAL'
}
扫描文件,返回filters列表中筛选的项。
@classmethod
def scan_files(cls, srcdir, filters, recursive=False, depth=1):
srcdirlen = len(srcdir.split(os.sep))
files = []
for parent, dirnames, filenames in os.walk(srcdir):
l = len(parent.split(os.sep))
if l - srcdirlen <= depth:
for fi in filters:
srcfilter = os.path.join(parent, fi)
ls = glob.glob(srcfilter, recursive=recursive)
files.extend(ls)
return files
txtfiles = self.scan_files(self.subworkdir('text'), '*.txt')
任务管理类TaskManager实现
语音合成是一个耗时的过程,采用后台线程实现,我们采用Python线程实现。TaskManager 就是为了维护任务的删除与增加,内部实现主要采用高效的queue.Queue。
class TaskManager:
def __init__(self, ui, maxtask=3):
self.tasks = queue.Queue()
self.intasks = []
# self._value_lock = threading.Lock()
# self.maxtask = maxtask
self.ui = ui
self.th = threading.Thread(target=do_combine_voice_task, args=(self,))
self.th.setDaemon(True)
self.th.start()
def add_task(self, task):
if task['docpath'] not in self.intasks:
# with self._value_lock:
self.intasks.append(task['docpath'])
self.tasks.put(task)
def remove_task(self, docpath):
# with self._value_lock:
self.intasks.remove(docpath)
...
程序运行就开启后台线程,等待从任务队列中实现获取合成任务,由于我需要动态删除任务,创建 self.intasks 辅助变量,Queue遍历是一个糟糕的问题。这也引入一个新问题,并发访问intasks会不会造成崩溃?对于我们操作不是非常频繁的方法,基本是不会。为了安全起见,采用threading.Lock()创建一个锁,保护执行线程会访问和界面会访问造成冲突的代码块。
结束队列,采用给队列里插入None标志,当取出None对象时,便认为任务结束。
do_combine_voice_task 实现
def do_combine_voice_task(taskmgr):
while True:
binterruput = False
task_ = taskmgr._task()
if task_ is None:
taskmgr.tasks.task_done()
return
doc_state = 'NORMAL'
docpath = task_['docpath']
wavpath = task_['wavpath']
tempwavpath = os.path.join(task_['cache'], Document.wavname(docpath))
try:
print('begin ', tempwavpath)
if os.path.exists(wavpath): os.remove(wavpath)
with wave.open(tempwavpath, 'w') as fp:
fp.setparams(TTSSample.wav_params)
nbytespersec = fp.getframerate() * fp.getnchannels() * fp.getsampwidth()
empty_vb = b''.join([b'0'] * int(nbytespersec * 0.5))
tts = TTSSample()
with open(docpath, 'r', encoding='utf-8') as f:
content = f.read()
lines = list(splitlines(content))
for idx, line in enumerate(lines, 1):
taskmgr.ui.change_docstate(docpath, 'DOING', '%%%d' % ((100 * idx) // len(lines)))
for param in taskmgr.sessionparams():
tts.session_begin_params = str(param)
text_to_speech(tts, fp, line)
fp.writeframes(empty_vb)
if taskmgr.isintask(docpath) is False:
doc_state = 'NORMAL'
taskmgr._task_done(docpath)
remove_file(tempwavpath, ignore=True)
binterruput = True
break
time.sleep(0.01)
if binterruput is True:
continue
shutil.move(tempwavpath, wavpath)
doc_state = 'DONE'
except Exception as e:
doc_state = 'FAIL'
traceback.print_exc()
finally:
print('end ', tempwavpath)
taskmgr.ui.change_docstate(docpath, doc_state)
taskmgr._task_done(docpath)
splitlines实现
按照 ‘?’, ‘?’, ‘!’, ‘-‘, ‘.’, ‘。’, ‘:’, ‘:’, ‘,’, ‘,’, ‘/’ 作为一句话分割,此处采用yiled实现,但是我在使用的时候,却直接转为list,简直是多此一举!。主要是为了统计进度其实也可以按照字数来统计,此处是为了简单,反正我不缺这点内存。
def splitlines(content):
def append_by(line):
temp_ = ''.join(line)
if len(temp_.strip()) > 2:
return temp_
def nextline(content, start):
line = []
idx = start
for idx, word in enumerate(content[start:], start):
if word in ['?', '?', '!', '-', '.', '。', ':', ':', ',', ',', '/']:
line.append(word)
break
elif word in ['\n', '\r']:
break
else:
line.append(word)
return idx + 1, append_by(line)
idx = 0
while True:
idx, line = nextline(content, idx)
if line is None or len(line) < 1:
if idx >= len(content):
break
else:
continue
yield line
Review代码反思
1、如何保护多线程访问,需要怎么保护访问变量才会恰到好处,不会过度保护或者裸奔?
2、由于界面只设计了设计模式,wav播放模式没有完成,要完成信号处理,两个全部堆在一块儿是不是给主框架窗口增加了负担,不便于扩展。是否考虑分开两种界面模式的代码?