目录
- TTS简介
- 安装需要的包
- UI界面
- 功能代码
- 语音工具类
- 窗体类
- 完整代码
TTS简介
TTS(Text To Speech)是一种语音合成技术,可以让机器将输入文本以语音的方式播放出来,实现机器说话的效果。
TTS分成语音处理及语音合成,先由机器识别输入的文字,再根据语音库进行语音合成。现在有很多可供调用的TTS接口,比如百度智能云的语音合成接口。微软在Windows系统中也提供了TTS的接口,可以调用此接口实现离线的TTS语音合成功能。
本文将使用pyttsx3库作为示范,编写一个语音合成小工具。
pyttsx3官方文档:https://pyttsx3.readthedocs.io
本文源码已上传至GitHub:
https://github.com/XMNHCAS/SpeechSynthesisTool
安装需要的包
安装PyQt5及其GUI设计工具
# 安装PyQt | |
pip install PyQt | |
# 安装PyQt设计器 | |
pip install PyQtDesigner |
本文使用的编辑器是VSCode,不是PyCharm,使用PyQt5的方式可能存在差异,具体使用时可以根据实际情况进行配置。
安装pyttsx3
pip install pyttsx
UI界面
可参考下图设计简单的GUI界面,由于本文主要为功能实例,故不考虑界面美观问题。
界面应有一个文本输入框,用以输入将要转化为语音的文本,同时需要一个播放按钮,用以触发语音播放的方法。语速、音量和语言可以按需选择。
使用PyQt5的设计工具,可以根据以上配置的GUI界面生成以下UI(XML)代码:
<ui version=".0"> | |
<class>Form</class> | |
<widget class="QWidget" name="Form"> | |
<property name="geometry"> | |
<rect> | |
<x></x> | |
<y></y> | |
<width></width> | |
<height></height> | |
</rect> | |
</property> | |
<property name="windowTitle"> | |
<string>语音合成器</string> | |
</property> | |
<property name="windowIcon"> | |
<iconset> | |
<normaloff>voice.ico</normaloff>voice.ico</iconset> | |
</property> | |
<widget class="QWidget" name="verticalLayoutWidget"> | |
<property name="geometry"> | |
<rect> | |
<x></x> | |
<y></y> | |
<width></width> | |
<height></height> | |
</rect> | |
</property> | |
<layout class="QVBoxLayout" name="verticalLayout"> | |
<property name="spacing"> | |
<number></number> | |
</property> | |
<item> | |
<layout class="QHBoxLayout" name="horizontalLayout_"> | |
<item> | |
<widget class="QLabel" name="label"> | |
<property name="text"> | |
<string>播报文本</string> | |
</property> | |
<property name="alignment"> | |
<set>Qt::AlignJustify|Qt::AlignTop</set> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<widget class="QTextEdit" name="tbx_text"/> | |
</item> | |
</layout> | |
</item> | |
<item> | |
<layout class="QHBoxLayout" name="horizontalLayout_"> | |
<item> | |
<widget class="QLabel" name="label_"> | |
<property name="text"> | |
<string>语速</string> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<widget class="QSlider" name="slider_rate"> | |
<property name="maximum"> | |
<number></number> | |
</property> | |
<property name="orientation"> | |
<enum>Qt::Horizontal</enum> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<widget class="QLabel" name="label_rate"> | |
<property name="minimumSize"> | |
<size> | |
<width></width> | |
<height></height> | |
</size> | |
</property> | |
<property name="text"> | |
<string></string> | |
</property> | |
<property name="alignment"> | |
<set>Qt::AlignCenter</set> | |
</property> | |
</widget> | |
</item> | |
</layout> | |
</item> | |
<item> | |
<layout class="QHBoxLayout" name="horizontalLayout_"> | |
<item> | |
<widget class="QLabel" name="label_"> | |
<property name="text"> | |
<string>音量</string> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<widget class="QSlider" name="slider_volumn"> | |
<property name="maximum"> | |
<number></number> | |
</property> | |
<property name="orientation"> | |
<enum>Qt::Horizontal</enum> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<widget class="QLabel" name="label_volumn"> | |
<property name="minimumSize"> | |
<size> | |
<width></width> | |
<height></height> | |
</size> | |
</property> | |
<property name="text"> | |
<string></string> | |
</property> | |
<property name="alignment"> | |
<set>Qt::AlignCenter</set> | |
</property> | |
</widget> | |
</item> | |
</layout> | |
</item> | |
<item> | |
<layout class="QHBoxLayout" name="horizontalLayout"> | |
<item> | |
<widget class="QLabel" name="label_"> | |
<property name="text"> | |
<string>选择语言</string> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<widget class="QRadioButton" name="rbtn_zh"> | |
<property name="text"> | |
<string>中文</string> | |
</property> | |
<property name="checked"> | |
<bool>true</bool> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<widget class="QRadioButton" name="rbtn_en"> | |
<property name="text"> | |
<string>英文</string> | |
</property> | |
</widget> | |
</item> | |
</layout> | |
</item> | |
<item> | |
<layout class="QHBoxLayout" name="horizontalLayout_"> | |
<item> | |
<widget class="QLabel" name="label_"> | |
<property name="minimumSize"> | |
<size> | |
<width></width> | |
<height></height> | |
</size> | |
</property> | |
<property name="text"> | |
<string/> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<widget class="QPushButton" name="btn_play"> | |
<property name="minimumSize"> | |
<size> | |
<width></width> | |
<height></height> | |
</size> | |
</property> | |
<property name="text"> | |
<string>播放</string> | |
</property> | |
</widget> | |
</item> | |
</layout> | |
</item> | |
</layout> | |
</widget> | |
</widget> | |
<resources/> | |
<connections/> | |
</ui> |
最后再使用PyQt5的界面工具,可以根据以上UI的代码,生成以下的窗体类:
# -*- coding: utf- -*- | |
# Form implementation generated from reading ui file 'd:\Program\VSCode\Python\TTS_PyQT\tts_form.ui' | |
# | |
# Created by: PyQt UI code generator 5.15.7 | |
# | |
# WARNING: Any manual changes made to this file will be lost when pyuic is | |
# run again. Do not edit this file unless you know what you are doing. | |
from PyQt import QtCore, QtGui, QtWidgets | |
class Ui_Form(object): | |
def setupUi(self, Form): | |
Form.setObjectName("Form") | |
Form.resize(, 284) | |
icon = QtGui.QIcon() | |
icon.addPixmap( | |
QtGui.QPixmap("./voice.ico"), | |
QtGui.QIcon.Normal, QtGui.QIcon.Off) | |
Form.setWindowIcon(icon) | |
self.verticalLayoutWidget = QtWidgets.QWidget(Form) | |
self.verticalLayoutWidget.setGeometry(QtCore.QRect(, 10, 291, 261)) | |
self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") | |
self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) | |
self.verticalLayout.setContentsMargins(, 0, 0, 0) | |
self.verticalLayout.setSpacing() | |
self.verticalLayout.setObjectName("verticalLayout") | |
self.horizontalLayout_ = QtWidgets.QHBoxLayout() | |
self.horizontalLayout_.setObjectName("horizontalLayout_2") | |
self.label = QtWidgets.QLabel(self.verticalLayoutWidget) | |
self.label.setAlignment(QtCore.Qt.AlignJustify | QtCore.Qt.AlignTop) | |
self.label.setObjectName("label") | |
self.horizontalLayout_.addWidget(self.label) | |
self.tbx_text = QtWidgets.QTextEdit(self.verticalLayoutWidget) | |
self.tbx_text.setObjectName("tbx_text") | |
self.horizontalLayout_.addWidget(self.tbx_text) | |
self.verticalLayout.addLayout(self.horizontalLayout_) | |
self.horizontalLayout_ = QtWidgets.QHBoxLayout() | |
self.horizontalLayout_.setObjectName("horizontalLayout_4") | |
self.label_ = QtWidgets.QLabel(self.verticalLayoutWidget) | |
self.label_.setObjectName("label_3") | |
self.horizontalLayout_.addWidget(self.label_3) | |
self.slider_rate = QtWidgets.QSlider(self.verticalLayoutWidget) | |
self.slider_rate.setMaximum() | |
self.slider_rate.setOrientation(QtCore.Qt.Horizontal) | |
self.slider_rate.setObjectName("slider_rate") | |
self.horizontalLayout_.addWidget(self.slider_rate) | |
self.label_rate = QtWidgets.QLabel(self.verticalLayoutWidget) | |
self.label_rate.setMinimumSize(QtCore.QSize(, 0)) | |
self.label_rate.setAlignment(QtCore.Qt.AlignCenter) | |
self.label_rate.setObjectName("label_rate") | |
self.horizontalLayout_.addWidget(self.label_rate) | |
self.verticalLayout.addLayout(self.horizontalLayout_) | |
self.horizontalLayout_ = QtWidgets.QHBoxLayout() | |
self.horizontalLayout_.setObjectName("horizontalLayout_3") | |
self.label_ = QtWidgets.QLabel(self.verticalLayoutWidget) | |
self.label_.setObjectName("label_2") | |
self.horizontalLayout_.addWidget(self.label_2) | |
self.slider_volumn = QtWidgets.QSlider(self.verticalLayoutWidget) | |
self.slider_volumn.setMaximum() | |
self.slider_volumn.setOrientation(QtCore.Qt.Horizontal) | |
self.slider_volumn.setObjectName("slider_volumn") | |
self.horizontalLayout_.addWidget(self.slider_volumn) | |
self.label_volumn = QtWidgets.QLabel(self.verticalLayoutWidget) | |
self.label_volumn.setMinimumSize(QtCore.QSize(, 0)) | |
self.label_volumn.setAlignment(QtCore.Qt.AlignCenter) | |
self.label_volumn.setObjectName("label_volumn") | |
self.horizontalLayout_.addWidget(self.label_volumn) | |
self.verticalLayout.addLayout(self.horizontalLayout_) | |
self.horizontalLayout = QtWidgets.QHBoxLayout() | |
self.horizontalLayout.setObjectName("horizontalLayout") | |
self.label_ = QtWidgets.QLabel(self.verticalLayoutWidget) | |
self.label_.setObjectName("label_4") | |
self.horizontalLayout.addWidget(self.label_) | |
self.rbtn_zh = QtWidgets.QRadioButton(self.verticalLayoutWidget) | |
self.rbtn_zh.setChecked(True) | |
self.rbtn_zh.setObjectName("rbtn_zh") | |
self.horizontalLayout.addWidget(self.rbtn_zh) | |
self.rbtn_en = QtWidgets.QRadioButton(self.verticalLayoutWidget) | |
self.rbtn_en.setObjectName("rbtn_en") | |
self.horizontalLayout.addWidget(self.rbtn_en) | |
self.verticalLayout.addLayout(self.horizontalLayout) | |
self.horizontalLayout_ = QtWidgets.QHBoxLayout() | |
self.horizontalLayout_.setObjectName("horizontalLayout_5") | |
self.label_ = QtWidgets.QLabel(self.verticalLayoutWidget) | |
self.label_.setMinimumSize(QtCore.QSize(60, 0)) | |
self.label_.setText("") | |
self.label_.setObjectName("label_5") | |
self.horizontalLayout_.addWidget(self.label_5) | |
self.btn_play = QtWidgets.QPushButton(self.verticalLayoutWidget) | |
self.btn_play.setMinimumSize(QtCore.QSize(, 30)) | |
self.btn_play.setObjectName("btn_play") | |
self.horizontalLayout_.addWidget(self.btn_play) | |
self.verticalLayout.addLayout(self.horizontalLayout_) | |
self.retranslateUi(Form) | |
QtCore.QMetaObject.connectSlotsByName(Form) | |
def retranslateUi(self, Form): | |
_translate = QtCore.QCoreApplication.translate | |
Form.setWindowTitle(_translate("Form", "语音合成器")) | |
self.label.setText(_translate("Form", "播报文本")) | |
self.label_.setText(_translate("Form", "语速")) | |
self.label_rate.setText(_translate("Form", "")) | |
self.label_.setText(_translate("Form", "音量")) | |
self.label_volumn.setText(_translate("Form", "")) | |
self.label_.setText(_translate("Form", "选择语言")) | |
self.rbtn_zh.setText(_translate("Form", "中文")) | |
self.rbtn_en.setText(_translate("Form", "英文")) | |
self.btn_play.setText(_translate("Form", "播放")) |
如果直接复制此代码,可能会出现图标丢失的问题。这个需要根据实际情况修改icon的配置,并添加要使用的ico图标文件。
功能代码
语音工具类
首先我们需要初始化并获取语音合成用的语音引擎对象。
# tts对象 | |
engine = pyttsx.init() |
我们可以通过该对象的setProperty方法,对语音合成的对象的属性进行修改:
属性名 | 解释 |
rate | 以每分钟字数表示的整数语速 |
volume | 音量,取值范围为[0.0, 1.0] |
voices | 语音的字符串标识符 |
语音工具类代码如下,代码含义可参考注释:
import pyttsx | |
class VoiceEngine(): | |
''' | |
tts 语音工具类 | |
''' | |
def __init__(self): | |
''' | |
初始化 | |
''' | |
# tts对象 | |
self.__engine = pyttsx.init() | |
# 语速 | |
self.__rate = | |
# 音量 | |
self.__volume = | |
# 语音ID,为中文,1为英文 | |
self.__voice = | |
def Rate(self): | |
''' | |
语速属性 | |
''' | |
return self.__rate | |
def Rate(self, value): | |
self.__rate = value | |
def Volume(self): | |
''' | |
音量属性 | |
''' | |
return self.__volume | |
def Volume(self, value): | |
self.__volume = value | |
def VoiceID(self): | |
''' | |
语音ID: -- 中文;1 -- 英文 | |
''' | |
return self.__voice | |
def VoiceID(self, value): | |
self.__voice = value | |
def Say(self, text): | |
''' | |
播放语音 | |
''' | |
self.__engine.setProperty('rate', self.__rate) | |
self.__engine.setProperty('volume', self.__volume) | |
# 获取可用语音列表,并设置语音 | |
voices = self.__engine.getProperty('voices') | |
self.__engine.setProperty('voice', voices[self.__voice].id) | |
# 保存语音文件 | |
# self.__engine.save_to_file(text, 'test.mp') | |
self.__engine.say(text) | |
self.__engine.runAndWait() | |
self.__engine.stop() |
窗体类
我们可以创建一个继承于我们刚刚创建的PyQt5的窗体类,并为窗体的拖拽事件和点击事件注册回调函数,同时创建一个语音工具类的实例,用以实现指定事件触发时需要执行的语音操作。
import sys | |
import _thread as th | |
from PyQt.QtWidgets import QMainWindow, QApplication | |
from Ui_tts_form import Ui_Form | |
class MainWindow(QMainWindow, Ui_Form): | |
''' | |
窗体类 | |
''' | |
def __init__(self, parent=None): | |
''' | |
初始化窗体 | |
''' | |
super(MainWindow, self).__init__(parent) | |
self.setupUi(self) | |
# 获取tts工具类实例 | |
self.engine = VoiceEngine() | |
self.__isPlaying = False | |
# 设置初始文本 | |
self.tbx_text.setText('床前明月光,疑似地上霜。\n举头望明月,低头思故乡。') | |
# 进度条数据绑定到label中显示 | |
self.slider_rate.valueChanged.connect(self.setRateTextValue) | |
self.slider_volumn.valueChanged.connect(self.setVolumnTextValue) | |
# 设置进度条初始值 | |
self.slider_rate.setValue(self.engine.Rate) | |
self.slider_volumn.setValue(self.engine.Volume) | |
# RadioButton选择事件 | |
self.rbtn_zh.toggled.connect(self.onSelectVoice_zh) | |
self.rbtn_en.toggled.connect(self.onSelectVoice_en) | |
# 播放按钮点击事件 | |
self.btn_play.clicked.connect(self.onPlayButtonClick) | |
def setRateTextValue(self): | |
''' | |
修改语速label文本值 | |
''' | |
value = self.slider_rate.value() | |
self.label_rate.setText(str(value)) | |
self.engine.Rate = value | |
def setVolumnTextValue(self): | |
''' | |
修改音量label文本值 | |
''' | |
value = self.slider_volumn.value() | |
self.label_volumn.setText(str(value /)) | |
self.engine.Volume = value | |
def onSelectVoice_zh(self): | |
''' | |
修改中文的语音配置及默认播放文本 | |
''' | |
self.tbx_text.setText('床前明月光,疑似地上霜。\n举头望明月,低头思故乡。') | |
self.engine.VoiceID = | |
def onSelectVoice_en(self): | |
''' | |
修改英文的语音配置及默认的播放文本 | |
''' | |
self.tbx_text.setText('Hello World') | |
self.engine.VoiceID = | |
def playVoice(self): | |
''' | |
播放 | |
''' | |
if self.__isPlaying is not True: | |
self.__isPlaying = True | |
text = self.tbx_text.toPlainText() | |
self.engine.Say(text) | |
self.__isPlaying = False | |
def onPlayButtonClick(self): | |
''' | |
播放按钮点击事件 | |
开启线程新线程播放语音,避免窗体因为语音播放而假卡死 | |
''' | |
th.start_new_thread(self.playVoice, ()) |
完整代码
import sys | |
import _thread as th | |
from PyQt.QtWidgets import QMainWindow, QApplication | |
from Ui_tts_form import Ui_Form | |
import pyttsx | |
class VoiceEngine(): | |
''' | |
tts 语音工具类 | |
''' | |
def __init__(self): | |
''' | |
初始化 | |
''' | |
# tts对象 | |
self.__engine = pyttsx.init() | |
# 语速 | |
self.__rate = | |
# 音量 | |
self.__volume = | |
# 语音ID,为中文,1为英文 | |
self.__voice = | |
def Rate(self): | |
''' | |
语速属性 | |
''' | |
return self.__rate | |
def Rate(self, value): | |
self.__rate = value | |
def Volume(self): | |
''' | |
音量属性 | |
''' | |
return self.__volume | |
def Volume(self, value): | |
self.__volume = value | |
def VoiceID(self): | |
''' | |
语音ID: -- 中文;1 -- 英文 | |
''' | |
return self.__voice | |
def VoiceID(self, value): | |
self.__voice = value | |
def Say(self, text): | |
''' | |
播放语音 | |
''' | |
self.__engine.setProperty('rate', self.__rate) | |
self.__engine.setProperty('volume', self.__volume) | |
voices = self.__engine.getProperty('voices') | |
self.__engine.setProperty('voice', voices[self.__voice]) | |
# 保存语音文件 | |
# self.__engine.save_to_file(text, 'test.mp') | |
self.__engine.say(text) | |
self.__engine.runAndWait() | |
self.__engine.stop() | |
class MainWindow(QMainWindow, Ui_Form): | |
''' | |
窗体类 | |
''' | |
def __init__(self, parent=None): | |
''' | |
初始化窗体 | |
''' | |
super(MainWindow, self).__init__(parent) | |
self.setupUi(self) | |
# 获取tts工具类实例 | |
self.engine = VoiceEngine() | |
self.__isPlaying = False | |
# 设置初始文本 | |
self.tbx_text.setText('床前明月光,疑似地上霜。\n举头望明月,低头思故乡。') | |
# 进度条数据绑定到label中显示 | |
self.slider_rate.valueChanged.connect(self.setRateTextValue) | |
self.slider_volumn.valueChanged.connect(self.setVolumnTextValue) | |
# 设置进度条初始值 | |
self.slider_rate.setValue(self.engine.Rate) | |
self.slider_volumn.setValue(self.engine.Volume) | |
# RadioButton选择事件 | |
self.rbtn_zh.toggled.connect(self.onSelectVoice_zh) | |
self.rbtn_en.toggled.connect(self.onSelectVoice_en) | |
# 播放按钮点击事件 | |
self.btn_play.clicked.connect(self.onPlayButtonClick) | |
def setRateTextValue(self): | |
''' | |
修改语速label文本值 | |
''' | |
value = self.slider_rate.value() | |
self.label_rate.setText(str(value)) | |
self.engine.Rate = value | |
def setVolumnTextValue(self): | |
''' | |
修改音量label文本值 | |
''' | |
value = self.slider_volumn.value() | |
self.label_volumn.setText(str(value /)) | |
self.engine.Volume = value | |
def onSelectVoice_zh(self): | |
''' | |
修改中文的语音配置及默认播放文本 | |
''' | |
self.tbx_text.setText('床前明月光,疑似地上霜。\n举头望明月,低头思故乡。') | |
self.engine.VoiceID = | |
def onSelectVoice_en(self): | |
''' | |
修改英文的语音配置及默认的播放文本 | |
''' | |
self.tbx_text.setText('Hello World') | |
self.engine.VoiceID = | |
def playVoice(self): | |
''' | |
播放 | |
''' | |
if self.__isPlaying is not True: | |
self.__isPlaying = True | |
text = self.tbx_text.toPlainText() | |
self.engine.Say(text) | |
self.__isPlaying = False | |
def onPlayButtonClick(self): | |
''' | |
修改语速label文本值 | |
''' | |
th.start_new_thread(self.playVoice, ()) | |
if __name__ == "__main__": | |
''' | |
主函数 | |
''' | |
app = QApplication(sys.argv) | |
form = MainWindow() | |
form.show() | |
sys.exit(app.exec_()) |